diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000000..0e268a507c --- /dev/null +++ b/.mailmap @@ -0,0 +1,2 @@ +Rob Harrington +Laura Summers diff --git a/Gemfile b/Gemfile index 07440249c5..b63961a3ef 100644 --- a/Gemfile +++ b/Gemfile @@ -15,6 +15,8 @@ gem 'spree_auth_devise', :github => 'spree/spree_auth_devise', :branch => '1-3-s gem 'spree_paypal_express', :github => "openfoodfoundation/better_spree_paypal_express", :branch => "1-3-stable" #gem 'spree_paypal_express', :github => "spree-contrib/better_spree_paypal_express", :branch => "1-3-stable" +gem 'delayed_job_active_record' +gem 'daemons' gem 'comfortable_mexican_sofa' # Fix bug in simple_form preventing collection_check_boxes usage within form_for block diff --git a/Gemfile.lock b/Gemfile.lock index 540c185899..17dbfdf8c5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -217,6 +217,7 @@ GEM safe_yaml (~> 0.9.0) css_parser (1.3.5) addressable + daemons (1.2.2) dalli (2.7.2) database_cleaner (0.7.1) db2fog (0.8.0) @@ -229,6 +230,11 @@ GEM debugger-ruby_core_source (~> 1.2.3) debugger-linecache (1.2.0) debugger-ruby_core_source (1.2.3) + delayed_job (4.0.4) + activesupport (>= 3.0, < 4.2) + delayed_job_active_record (4.0.2) + activerecord (>= 3.0, < 4.2) + delayed_job (>= 3.0, < 4.1) devise (2.2.8) bcrypt-ruby (~> 3.0) orm_adapter (~> 0.1) @@ -334,7 +340,7 @@ GEM mail (2.5.4) mime-types (~> 1.16) treetop (~> 1.4.8) - method_source (0.8.1) + method_source (0.8.2) mime-types (1.25.1) mini_portile (0.6.2) momentjs-rails (2.5.1) @@ -518,7 +524,7 @@ GEM xml-simple (1.1.4) xpath (2.0.0) nokogiri (~> 1.3) - zeus (0.13.3) + zeus (0.15.4) method_source (>= 0.6.7) PLATFORMS @@ -538,11 +544,13 @@ DEPENDENCIES comfortable_mexican_sofa compass-rails custom_error_message! + daemons dalli database_cleaner (= 0.7.1) db2fog debugger-linecache deface! + delayed_job_active_record factory_girl_rails figaro foreigner diff --git a/README.markdown b/README.markdown index af194a0083..07f786a453 100644 --- a/README.markdown +++ b/README.markdown @@ -95,4 +95,4 @@ usage instructions. ## Licence -Copyright (c) 2012 - 2013 Open Food Foundation, released under the AGPL licence. +Copyright (c) 2012 - 2015 Open Food Foundation, released under the AGPL licence. diff --git a/app/assets/images/noimage/group.png b/app/assets/images/noimage/group.png new file mode 100644 index 0000000000..db71856deb Binary files /dev/null and b/app/assets/images/noimage/group.png differ diff --git a/app/assets/images/open-food-network-beta-black.png b/app/assets/images/open-food-network-beta-black.png new file mode 100644 index 0000000000..8e5f810023 Binary files /dev/null and b/app/assets/images/open-food-network-beta-black.png differ diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 78ff8306f8..9a07309352 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -19,6 +19,7 @@ //= require ../shared/ng-infinite-scroll.min.js //= require ./admin //= require ./enterprises/enterprises +//= require ./enterprise_groups/enterprise_groups //= require ./payment_methods/payment_methods //= require ./products/products //= require ./shipping_methods/shipping_methods diff --git a/app/assets/javascripts/admin/enterprise_groups/controllers/enterprise_group_controller.js.coffee b/app/assets/javascripts/admin/enterprise_groups/controllers/enterprise_group_controller.js.coffee new file mode 100644 index 0000000000..c207032853 --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_groups/controllers/enterprise_group_controller.js.coffee @@ -0,0 +1,3 @@ +angular.module("admin.enterprise_groups") + .controller "enterpriseGroupCtrl", ($scope, SideMenu) -> + $scope.menu = SideMenu diff --git a/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee new file mode 100644 index 0000000000..7b9a8165a1 --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_groups/controllers/side_menu_controller.js.coffee @@ -0,0 +1,15 @@ +angular.module("admin.enterprise_groups") + .controller "sideMenuCtrl", ($scope, SideMenu) -> + $scope.menu = SideMenu + $scope.select = SideMenu.select + + $scope.menu.setItems [ + { name: 'Primary Details', icon_class: "icon-user" } + { name: 'Users', icon_class: "icon-user" } + { name: 'About', icon_class: "icon-pencil" } + { name: 'Images', icon_class: "icon-picture" } + { name: 'Contact', icon_class: "icon-phone" } + { name: 'Web', icon_class: "icon-globe" } + ] + + $scope.select(0) diff --git a/app/assets/javascripts/admin/enterprise_groups/enterprise_groups.js.coffee b/app/assets/javascripts/admin/enterprise_groups/enterprise_groups.js.coffee new file mode 100644 index 0000000000..0ff8e4f515 --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_groups/enterprise_groups.js.coffee @@ -0,0 +1 @@ +angular.module("admin.enterprise_groups", ["admin.side_menu", "admin.users", "textAngular"]) diff --git a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee index a74c3005e0..d5d0e1681a 100644 --- a/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee +++ b/app/assets/javascripts/admin/enterprises/controllers/side_menu_controller.js.coffee @@ -13,6 +13,7 @@ angular.module("admin.enterprises") { name: 'About', icon_class: "icon-pencil" } { name: 'Business Details', icon_class: "icon-briefcase" } { name: 'Images', icon_class: "icon-picture" } + { name: "Properties", icon_class: "icon-tags", show: "showProperties()" } { name: "Shipping Methods", icon_class: "icon-truck", show: "showShippingMethods()" } { name: "Payment Methods", icon_class: "icon-money", show: "showPaymentMethods()" } { name: "Enterprise Fees", icon_class: "icon-tasks", show: "showEnterpriseFees()" } @@ -28,6 +29,9 @@ angular.module("admin.enterprises") else true + $scope.showProperties = -> + !!$scope.Enterprise.is_primary_producer + $scope.showShippingMethods = -> enterprisePermissions.can_manage_shipping_methods && $scope.Enterprise.sells != "none" diff --git a/app/assets/javascripts/admin/order_cycle.js.erb.coffee b/app/assets/javascripts/admin/order_cycle.js.erb.coffee index 608de85d8a..2b5ca05f42 100644 --- a/app/assets/javascripts/admin/order_cycle.js.erb.coffee +++ b/app/assets/javascripts/admin/order_cycle.js.erb.coffee @@ -1,10 +1,10 @@ angular.module('admin.order_cycles', ['ngResource']) - .controller('AdminCreateOrderCycleCtrl', ['$scope', 'OrderCycle', 'Enterprise', 'EnterpriseFee', ($scope, OrderCycle, Enterprise, EnterpriseFee) -> - $scope.enterprises = Enterprise.index() + .controller('AdminCreateOrderCycleCtrl', ['$scope', '$filter', 'OrderCycle', 'Enterprise', 'EnterpriseFee', 'ocInstance', ($scope, $filter, OrderCycle, Enterprise, EnterpriseFee, ocInstance) -> + $scope.enterprises = Enterprise.index(coordinator_id: ocInstance.coordinator_id) $scope.supplied_products = Enterprise.supplied_products - $scope.enterprise_fees = EnterpriseFee.index() + $scope.enterprise_fees = EnterpriseFee.index(coordinator_id: ocInstance.coordinator_id) - $scope.order_cycle = OrderCycle.order_cycle + $scope.order_cycle = OrderCycle.new({ coordinator_id: ocInstance.coordinator_id}) $scope.loaded = -> Enterprise.loaded && EnterpriseFee.loaded @@ -27,14 +27,14 @@ angular.module('admin.order_cycles', ['ngResource']) $scope.variantSuppliedToOrderCycle = (variant) -> OrderCycle.variantSuppliedToOrderCycle(variant) - $scope.incomingExchangesVariants = -> - OrderCycle.incomingExchangesVariants() + $scope.incomingExchangeVariantsFor = (enterprise_id) -> + $filter('filterExchangeVariants')(OrderCycle.incomingExchangesVariants(), $scope.order_cycle.visible_variants_for_outgoing_exchanges[enterprise_id]) $scope.exchangeDirection = (exchange) -> OrderCycle.exchangeDirection(exchange) - $scope.participatingEnterprises = -> - $scope.enterprises[id] for id in OrderCycle.participatingEnterpriseIds() + $scope.enterprisesWithFees = -> + $scope.enterprises[id] for id in OrderCycle.participatingEnterpriseIds() when $scope.enterpriseFeesForEnterprise(id).length > 0 $scope.toggleProducts = ($event, exchange) -> $event.preventDefault() @@ -79,12 +79,12 @@ angular.module('admin.order_cycles', ['ngResource']) OrderCycle.create() ]) - .controller('AdminEditOrderCycleCtrl', ['$scope', '$location', 'OrderCycle', 'Enterprise', 'EnterpriseFee', ($scope, $location, OrderCycle, Enterprise, EnterpriseFee) -> - $scope.enterprises = Enterprise.index() - $scope.supplied_products = Enterprise.supplied_products - $scope.enterprise_fees = EnterpriseFee.index() - + .controller('AdminEditOrderCycleCtrl', ['$scope', '$filter', '$location', 'OrderCycle', 'Enterprise', 'EnterpriseFee', ($scope, $filter, $location, OrderCycle, Enterprise, EnterpriseFee) -> order_cycle_id = $location.absUrl().match(/\/admin\/order_cycles\/(\d+)/)[1] + $scope.enterprises = Enterprise.index(order_cycle_id: order_cycle_id) + $scope.supplied_products = Enterprise.supplied_products + $scope.enterprise_fees = EnterpriseFee.index(order_cycle_id: order_cycle_id) + $scope.order_cycle = OrderCycle.load(order_cycle_id) $scope.loaded = -> @@ -108,14 +108,14 @@ angular.module('admin.order_cycles', ['ngResource']) $scope.variantSuppliedToOrderCycle = (variant) -> OrderCycle.variantSuppliedToOrderCycle(variant) - $scope.incomingExchangesVariants = -> - OrderCycle.incomingExchangesVariants() + $scope.incomingExchangeVariantsFor = (enterprise_id) -> + $filter('filterExchangeVariants')(OrderCycle.incomingExchangesVariants(), $scope.order_cycle.visible_variants_for_outgoing_exchanges[enterprise_id]) $scope.exchangeDirection = (exchange) -> OrderCycle.exchangeDirection(exchange) - $scope.participatingEnterprises = -> - $scope.enterprises[id] for id in OrderCycle.participatingEnterpriseIds() + $scope.enterprisesWithFees = -> + $scope.enterprises[id] for id in OrderCycle.participatingEnterpriseIds() when $scope.enterpriseFeesForEnterprise(id).length > 0 $scope.toggleProducts = ($event, exchange) -> $event.preventDefault() diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee index b93d464d74..771902ca5b 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple_create.js.coffee @@ -1,8 +1,9 @@ -angular.module('admin.order_cycles').controller "AdminSimpleCreateOrderCycleCtrl", ($scope, OrderCycle, Enterprise, EnterpriseFee) -> - $scope.enterprises = Enterprise.index (enterprises) => - $scope.init(enterprises) - $scope.enterprise_fees = EnterpriseFee.index() - $scope.order_cycle = OrderCycle.order_cycle +angular.module('admin.order_cycles').controller "AdminSimpleCreateOrderCycleCtrl", ($scope, OrderCycle, Enterprise, EnterpriseFee, ocInstance) -> + $scope.order_cycle = OrderCycle.new {coordinator_id: ocInstance.coordinator_id}, => + # TODO: make this a get method, which only fetches one enterprise + $scope.enterprises = Enterprise.index {coordinator_id: ocInstance.coordinator_id}, (enterprises) => + $scope.init(enterprises) + $scope.enterprise_fees = EnterpriseFee.index(coordinator_id: ocInstance.coordinator_id) $scope.init = (enterprises) -> enterprise = enterprises[Object.keys(enterprises)[0]] diff --git a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee index 84f2fcf74a..cab677407e 100644 --- a/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/controllers/simple_edit.js.coffee @@ -2,8 +2,8 @@ angular.module('admin.order_cycles').controller "AdminSimpleEditOrderCycleCtrl", $scope.orderCycleId = -> $location.absUrl().match(/\/admin\/order_cycles\/(\d+)/)[1] - $scope.enterprises = Enterprise.index() - $scope.enterprise_fees = EnterpriseFee.index() + $scope.enterprises = Enterprise.index(order_cycle_id: $scope.orderCycleId()) + $scope.enterprise_fees = EnterpriseFee.index(order_cycle_id: $scope.orderCycleId()) $scope.order_cycle = OrderCycle.load $scope.orderCycleId(), (order_cycle) => $scope.init() diff --git a/app/assets/javascripts/admin/order_cycles/filters/filter_exchange_variants.js.coffee b/app/assets/javascripts/admin/order_cycles/filters/filter_exchange_variants.js.coffee new file mode 100644 index 0000000000..2b22b1d60c --- /dev/null +++ b/app/assets/javascripts/admin/order_cycles/filters/filter_exchange_variants.js.coffee @@ -0,0 +1,6 @@ +angular.module("admin.order_cycles").filter "filterExchangeVariants", -> + return (variants, rules) -> + if variants? && rules? + return (variant for variant in variants when variant in rules) + else + return [] diff --git a/app/assets/javascripts/admin/order_cycles/filters/visible_product_variants.js.coffee b/app/assets/javascripts/admin/order_cycles/filters/visible_product_variants.js.coffee new file mode 100644 index 0000000000..8989af5b9d --- /dev/null +++ b/app/assets/javascripts/admin/order_cycles/filters/visible_product_variants.js.coffee @@ -0,0 +1,4 @@ +angular.module("admin.order_cycles").filter "visibleProductVariants", -> + return (product, exchange, rules) -> + variants = product.variants.concat( [{ "id": product.master_id}] ) + return (variant for variant in variants when variant.id in rules[exchange.enterprise_id]) diff --git a/app/assets/javascripts/admin/order_cycles/filters/visible_products.js.coffee b/app/assets/javascripts/admin/order_cycles/filters/visible_products.js.coffee new file mode 100644 index 0000000000..3afff183a1 --- /dev/null +++ b/app/assets/javascripts/admin/order_cycles/filters/visible_products.js.coffee @@ -0,0 +1,3 @@ +angular.module("admin.order_cycles").filter "visibleProducts", ($filter) -> + return (products, exchange, rules) -> + return (product for product in products when $filter('visibleProductVariants')(product, exchange, rules).length > 0) diff --git a/app/assets/javascripts/admin/order_cycles/services/enterprise.js.coffee b/app/assets/javascripts/admin/order_cycles/services/enterprise.js.coffee index 244d050aba..5e40f621dc 100644 --- a/app/assets/javascripts/admin/order_cycles/services/enterprise.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/services/enterprise.js.coffee @@ -1,24 +1,28 @@ angular.module('admin.order_cycles').factory('Enterprise', ($resource) -> - Enterprise = $resource('/admin/enterprises/for_order_cycle/:enterprise_id.json', {}, {'index': {method: 'GET', isArray: true}}) - + Enterprise = $resource('/admin/enterprises/for_order_cycle/:enterprise_id.json', {}, { + 'index': + method: 'GET' + isArray: true + params: + order_cycle_id: '@order_cycle_id' + coordinator_id: '@coordinator_id' + }) { Enterprise: Enterprise enterprises: {} supplied_products: [] loaded: false - index: (callback=null) -> - service = this - - Enterprise.index (data) -> + index: (params={}, callback=null) -> + Enterprise.index params, (data) => for enterprise in data - service.enterprises[enterprise.id] = enterprise + @enterprises[enterprise.id] = enterprise for product in enterprise.supplied_products - service.supplied_products.push(product) + @supplied_products.push(product) - service.loaded = true - (callback || angular.noop)(service.enterprises) + @loaded = true + (callback || angular.noop)(@enterprises) this.enterprises @@ -40,4 +44,4 @@ angular.module('admin.order_cycles').factory('Enterprise', ($resource) -> numVariants += if product.variants.length == 0 then 1 else product.variants.length numVariants - }) \ No newline at end of file + }) diff --git a/app/assets/javascripts/admin/order_cycles/services/enterprise_fee.js.coffee b/app/assets/javascripts/admin/order_cycles/services/enterprise_fee.js.coffee index 330d7c031e..fe443a7cf5 100644 --- a/app/assets/javascripts/admin/order_cycles/services/enterprise_fee.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/services/enterprise_fee.js.coffee @@ -1,18 +1,23 @@ angular.module('admin.order_cycles').factory('EnterpriseFee', ($resource) -> - EnterpriseFee = $resource('/admin/enterprise_fees/:enterprise_fee_id.json', {}, {'index': {method: 'GET', isArray: true}}) + EnterpriseFee = $resource('/admin/enterprise_fees/for_order_cycle/:enterprise_fee_id.json', {}, { + 'index': + method: 'GET' + isArray: true + params: + order_cycle_id: '@order_cycle_id' + coordinator_id: '@coordinator_id' + }) { EnterpriseFee: EnterpriseFee enterprise_fees: {} loaded: false - index: -> - service = this - EnterpriseFee.index (data) -> - service.enterprise_fees = data - service.loaded = true + index: (params={}) -> + EnterpriseFee.index params, (data) => + @enterprise_fees = data + @loaded = true forEnterprise: (enterprise_id) -> - enterprise_fee for enterprise_fee in this.enterprise_fees when enterprise_fee.enterprise_id == enterprise_id + enterprise_fee for enterprise_fee in @enterprise_fees when enterprise_fee.enterprise_id == enterprise_id }) - diff --git a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee index 40197452bf..95dc62ca4e 100644 --- a/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/services/order_cycle.js.coffee @@ -1,14 +1,12 @@ angular.module('admin.order_cycles').factory('OrderCycle', ($resource, $window) -> - OrderCycle = $resource '/admin/order_cycles/:order_cycle_id.json', {}, { + OrderCycle = $resource '/admin/order_cycles/:action_name/:order_cycle_id.json', {}, { 'index': { method: 'GET', isArray: true} + 'new' : { method: 'GET', params: { action_name: "new" } } 'create': { method: 'POST'} 'update': { method: 'PUT'}} { - order_cycle: - incoming_exchanges: [] - outgoing_exchanges: [] - coordinator_fees: [] + order_cycle: {} loaded: false @@ -24,7 +22,9 @@ angular.module('admin.order_cycles').factory('OrderCycle', ($resource, $window) exchange.showProducts = !exchange.showProducts setExchangeVariants: (exchange, variants, selected) -> - exchange.variants[variant] = selected for variant in variants + direction = if exchange.incoming then "incoming" else "outgoing" + editable = @order_cycle["editable_variants_for_#{direction}_exchanges"][exchange.enterprise_id] || [] + exchange.variants[variant] = selected for variant in variants when variant in editable addSupplier: (new_supplier_id) -> this.order_cycle.incoming_exchanges.push({enterprise_id: new_supplier_id, incoming: true, active: true, variants: {}, enterprise_fees: []}) @@ -84,6 +84,20 @@ angular.module('admin.order_cycles').factory('OrderCycle', ($resource, $window) for exchange in this.order_cycle.outgoing_exchanges exchange.variants[variant_id] = false + new: (params, callback=null) -> + OrderCycle.new params, (oc) => + delete oc.$promise + delete oc.$resolved + angular.extend(@order_cycle, oc) + @order_cycle.incoming_exchanges = [] + @order_cycle.outgoing_exchanges = [] + delete(@order_cycle.exchanges) + @loaded = true + + (callback || angular.noop)(@order_cycle) + + @order_cycle + load: (order_cycle_id, callback=null) -> service = this OrderCycle.get {order_cycle_id: order_cycle_id}, (oc) -> @@ -127,6 +141,7 @@ angular.module('admin.order_cycles').factory('OrderCycle', ($resource, $window) dataForSubmit: -> data = this.deepCopy() + data = this.stripNonSubmittableAttributes(data) data = this.removeInactiveExchanges(data) data = this.translateCoordinatorFees(data) data = this.translateExchangeFees(data) @@ -147,6 +162,14 @@ angular.module('admin.order_cycles').factory('OrderCycle', ($resource, $window) data + stripNonSubmittableAttributes: (order_cycle) -> + delete order_cycle.id + delete order_cycle.viewing_as_coordinator + delete order_cycle.editable_variants_for_incoming_exchanges + delete order_cycle.editable_variants_for_outgoing_exchanges + delete order_cycle.visible_variants_for_outgoing_exchanges + order_cycle + removeInactiveExchanges: (order_cycle) -> order_cycle.incoming_exchanges = (exchange for exchange in order_cycle.incoming_exchanges when exchange.active) diff --git a/app/assets/javascripts/darkswarm/cart.js.coffee b/app/assets/javascripts/darkswarm/cart.js.coffee index cd12c67c4a..471991143a 100644 --- a/app/assets/javascripts/darkswarm/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/cart.js.coffee @@ -11,9 +11,9 @@ $ -> # Temporarily handles the cart showing stuff $(document).ready -> - $('#cart_adjustments').hide() + $('.cart_adjustment').hide() - $('th.cart-adjustment-header a').click -> - $('#cart_adjustments').toggle() + $('td.cart-adjustments a').click -> + $('.cart_adjustment').toggle() $(this).html('Item Handling Fees (included in item totals)') false diff --git a/app/assets/javascripts/darkswarm/controllers/group_enterprise_node_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/group_enterprise_node_controller.js.coffee new file mode 100644 index 0000000000..376320e458 --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/group_enterprise_node_controller.js.coffee @@ -0,0 +1,12 @@ +Darkswarm.controller "GroupEnterpriseNodeCtrl", ($scope, CurrentHub) -> + + $scope.active = false + + $scope.toggle = -> + $scope.active = !$scope.active + + $scope.open = -> + $scope.active + + $scope.current = -> + $scope.hub.id is CurrentHub.hub.id diff --git a/app/assets/javascripts/darkswarm/controllers/group_enterprises_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/group_enterprises_controller.js.coffee new file mode 100644 index 0000000000..db1a9a1d8b --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/group_enterprises_controller.js.coffee @@ -0,0 +1,12 @@ +Darkswarm.controller "GroupEnterprisesCtrl", ($scope, Search, FilterSelectorsService) -> + $scope.totalActive = FilterSelectorsService.totalActive + $scope.clearAll = FilterSelectorsService.clearAll + $scope.filterText = FilterSelectorsService.filterText + $scope.FilterSelectorsService = FilterSelectorsService + $scope.query = Search.search() + $scope.activeTaxons = [] + $scope.show_profiles = false + $scope.filtersActive = false + + $scope.$watch "query", (query)-> + Search.search query diff --git a/app/assets/javascripts/darkswarm/controllers/group_page_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/group_page_controller.js.coffee new file mode 100644 index 0000000000..b30d409b97 --- /dev/null +++ b/app/assets/javascripts/darkswarm/controllers/group_page_controller.js.coffee @@ -0,0 +1,14 @@ +Darkswarm.controller "GroupPageCtrl", ($scope, group_enterprises, Enterprises, MapConfiguration, OfnMap) -> + $scope.Enterprises = Enterprises + + group_enterprises_ids = group_enterprises.map (enterprise) => + enterprise.id + is_in_group = (enterprise) -> + group_enterprises_ids.indexOf(enterprise.id) != -1 + + $scope.group_producers = Enterprises.producers.filter is_in_group + $scope.group_hubs = Enterprises.hubs.filter is_in_group + + $scope.map = angular.copy MapConfiguration.options + $scope.mapMarkers = OfnMap.enterprise_markers group_enterprises + diff --git a/app/assets/javascripts/darkswarm/controllers/map_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/map_controller.js.coffee index 7b0cd3607b..853e86d510 100644 --- a/app/assets/javascripts/darkswarm/controllers/map_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/map_controller.js.coffee @@ -1,3 +1,3 @@ Darkswarm.controller "MapCtrl", ($scope, MapConfiguration, OfnMap)-> $scope.OfnMap = OfnMap - $scope.map = MapConfiguration.options + $scope.map = angular.copy MapConfiguration.options diff --git a/app/assets/javascripts/darkswarm/controllers/registration/registration_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/registration/registration_controller.js.coffee index 3be0378619..29a60c3f15 100644 --- a/app/assets/javascripts/darkswarm/controllers/registration/registration_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/registration/registration_controller.js.coffee @@ -3,7 +3,7 @@ Darkswarm.controller "RegistrationCtrl", ($scope, RegistrationService, Enterpris $scope.enterprise = EnterpriseRegistrationService.enterprise $scope.select = RegistrationService.select - $scope.steps = ['details','contact','type','about','images','social'] + $scope.steps = ['details', 'contact', 'type', 'about', 'images', 'social'] $scope.countries = availableCountries diff --git a/app/assets/javascripts/darkswarm/controllers/tabs_controller.js.coffee b/app/assets/javascripts/darkswarm/controllers/tabs_controller.js.coffee index 0bbe0bbd23..fad13164ff 100644 --- a/app/assets/javascripts/darkswarm/controllers/tabs_controller.js.coffee +++ b/app/assets/javascripts/darkswarm/controllers/tabs_controller.js.coffee @@ -1,10 +1,14 @@ -Darkswarm.controller "TabsCtrl", ($scope, $rootScope, $location, OrderCycle) -> +Darkswarm.controller "TabsCtrl", ($scope, $rootScope, $location) -> # Return active if supplied path matches url hash path. $scope.active = (path)-> $location.hash() == path - # Toggle tab selected status by setting the url hash path. + # Select tab by setting the url hash path. $scope.select = (path)-> + $location.hash path + + # Toggle tab selected status by setting the url hash path. + $scope.toggle = (path)-> if $scope.active(path) $location.hash "" else diff --git a/app/assets/javascripts/darkswarm/directives/link_to_service.js.coffee b/app/assets/javascripts/darkswarm/directives/link_to_service.js.coffee new file mode 100644 index 0000000000..dc0f513588 --- /dev/null +++ b/app/assets/javascripts/darkswarm/directives/link_to_service.js.coffee @@ -0,0 +1,8 @@ +Darkswarm.directive "linkToService", -> + restrict: 'E' + replace: true + scope: { + ref: '=' + service: '=' + } + template: '' diff --git a/app/assets/javascripts/darkswarm/filters/ext_url.js.coffee b/app/assets/javascripts/darkswarm/filters/ext_url.js.coffee new file mode 100644 index 0000000000..38eed7c294 --- /dev/null +++ b/app/assets/javascripts/darkswarm/filters/ext_url.js.coffee @@ -0,0 +1,7 @@ +Darkswarm.filter "ext_url", -> + urlPattern = /^https?:\/\// + (url, prefix) -> + if !url || url.match(urlPattern) + url + else + prefix + url diff --git a/app/assets/javascripts/darkswarm/filters/shipping.js.coffee b/app/assets/javascripts/darkswarm/filters/shipping.js.coffee index 761e34337d..7879568f0d 100644 --- a/app/assets/javascripts/darkswarm/filters/shipping.js.coffee +++ b/app/assets/javascripts/darkswarm/filters/shipping.js.coffee @@ -1,9 +1,10 @@ -Darkswarm.filter 'shipping', ()-> +Darkswarm.filter 'shipping', ()-> (objects, options)-> objects ||= [] - options ?= null - - if options.pickup and !options.delivery + + if !options + objects + else if options.pickup and !options.delivery objects.filter (obj)-> obj.pickup else if options.delivery and !options.pickup diff --git a/app/assets/javascripts/darkswarm/services/cart.js.coffee b/app/assets/javascripts/darkswarm/services/cart.js.coffee index c519d22fdd..bbb0b5fab4 100644 --- a/app/assets/javascripts/darkswarm/services/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/services/cart.js.coffee @@ -8,6 +8,7 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> for line_item in @line_items line_item.variant.line_item = line_item Variants.register line_item.variant + line_item.variant.extended_name = @extendedVariantName(line_item.variant) orderChanged: => @unsaved() @@ -63,8 +64,17 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)-> @create_line_item(variant) unless exists create_line_item: (variant)-> + variant.extended_name = @extendedVariantName(variant) variant.line_item = variant: variant quantity: null max_quantity: null - @line_items.push variant.line_item \ No newline at end of file + @line_items.push variant.line_item + + extendedVariantName: (variant) => + if variant.product_name == variant.name_to_display + variant.product_name + else + name = "#{variant.product_name} - #{variant.name_to_display}" + name += " (#{variant.options_text})" if variant.options_text + name diff --git a/app/assets/javascripts/darkswarm/services/map.js.coffee b/app/assets/javascripts/darkswarm/services/map.js.coffee index 703c3c54bf..7ff9f553f2 100644 --- a/app/assets/javascripts/darkswarm/services/map.js.coffee +++ b/app/assets/javascripts/darkswarm/services/map.js.coffee @@ -1,11 +1,13 @@ -Darkswarm.factory "OfnMap", (Enterprises, EnterpriseModal, visibleFilter)-> +Darkswarm.factory "OfnMap", (Enterprises, EnterpriseModal, visibleFilter) -> new class OfnMap constructor: -> - @enterprises = (@extend(enterprise) for enterprise in visibleFilter(Enterprises.enterprises)) + @enterprises = @enterprise_markers(Enterprises.enterprises) + enterprise_markers: (enterprises) -> + @extend(enterprise) for enterprise in visibleFilter(enterprises) # Adding methods to each enterprise - extend: (enterprise)-> + extend: (enterprise) -> new class MapMarker # We're whitelisting attributes because GMaps tries to crawl # our data, and our data is recursive, so it breaks diff --git a/app/assets/stylesheets/admin/side_menu.css.sass b/app/assets/stylesheets/admin/side_menu.css.sass index 6c03059d27..c218cfbcf0 100644 --- a/app/assets/stylesheets/admin/side_menu.css.sass +++ b/app/assets/stylesheets/admin/side_menu.css.sass @@ -7,9 +7,9 @@ font-size: 120% cursor: pointer text-transform: uppercase - &.odd + &:nth-child(odd) background-color: #ebf3fb - &.even + &:nth-child(even) background-color: #ffffff &:hover background-color: #eaf0f5 diff --git a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass index b50586ab6e..38b77dd756 100644 --- a/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-popovers.css.sass @@ -75,7 +75,7 @@ ordercycle button.graph-button - z-index: 9999999 + // z-index: 9999999 border: 1px solid transparent padding: 0 margin: 0 diff --git a/app/assets/stylesheets/darkswarm/active_table.css.sass b/app/assets/stylesheets/darkswarm/active_table.css.sass index f3a3f0900a..b8280d22ca 100644 --- a/app/assets/stylesheets/darkswarm/active_table.css.sass +++ b/app/assets/stylesheets/darkswarm/active_table.css.sass @@ -30,21 +30,20 @@ text-decoration: underline span.margin-top - margin-top: 0.5rem + margin-top: 0.5rem display: inline-block // Generic text resize - @media all and (max-width: 640px) + @media all and (max-width: 640px) &, & * - font-size: 0.875rem + font-size: 0.875rem fat > div label - &, & * - font-size: 0.75rem + &, & * + font-size: 0.75rem - - .active_table_row - // Inherits from active_table - border: 1px solid transparent + // Inherits from active_table + .active_table_row + border: 1px solid transparent @include border-radius(0.5em) // Foundation overrides @@ -77,15 +76,15 @@ .active_table_row:last-child border-bottom: 1px solid $disabled-bright @include border-radius-mixed(0, 0, 0.5em, 0.5em) - - + + //Open row sections .fat > div border-top: 1px solid #aaa - @media all and (max-width: 640px) + @media all and (max-width: 640px) margin-top: 1em - ul, ol + ul, ol font-size: 0.875rem [class*="block-grid-"] > li @@ -97,10 +96,10 @@ margin-top: 0.25rem margin-bottom: 0.25rem color: #777 - + p.trans-sentence text-transform: capitalize - + &.closed &:hover, &:active, &:focus .active_table_row.closed @@ -113,7 +112,3 @@ &.open .active_table_row:first-child color: $dark-grey - - - - diff --git a/app/assets/stylesheets/darkswarm/branding.css.sass b/app/assets/stylesheets/darkswarm/branding.css.sass index 71760e23fd..da2600818e 100644 --- a/app/assets/stylesheets/darkswarm/branding.css.sass +++ b/app/assets/stylesheets/darkswarm/branding.css.sass @@ -25,5 +25,6 @@ $disabled-v-dark: #808080 $med-grey: #666 $med-drk-grey: #444 $dark-grey: #333 +$light-grey: #ddd $black: #000 diff --git a/app/assets/stylesheets/darkswarm/checkout.css.sass b/app/assets/stylesheets/darkswarm/checkout.css.sass index 77feb9c44e..d5a2cb8104 100644 --- a/app/assets/stylesheets/darkswarm/checkout.css.sass +++ b/app/assets/stylesheets/darkswarm/checkout.css.sass @@ -2,6 +2,11 @@ @import branding @import animations +.order-summary + background-color: #e1f0f5 + padding: 1em + width: 100% + checkout display: block @@ -55,7 +60,6 @@ checkout text-align: left // Logic to swap out up / down accordion icons - //Foundation overrides dd > a @include csstrans diff --git a/app/assets/stylesheets/darkswarm/groups.css.sass b/app/assets/stylesheets/darkswarm/groups.css.sass index b859273ab7..7f70bf7211 100644 --- a/app/assets/stylesheets/darkswarm/groups.css.sass +++ b/app/assets/stylesheets/darkswarm/groups.css.sass @@ -1,6 +1,7 @@ @import branding @import mixins +// Search page #groups background-color: $clr-brick-light background-image: url("/assets/groups.svg") @@ -8,35 +9,97 @@ background-repeat: no-repeat padding-bottom: 20px + a > .group-name + &:hover, &:focus, &:active + text-decoration: underline + + .groups-icons + text-align: right + a + font-size: 1.5em + + .groups-header + border: 2px solid $clr-brick-light-bright + @include border-radius-mixed(0.5em, 0.5em, 0, 0) + margin: -1rem 0 1rem + padding: 1rem 0.9375rem + @media screen and (min-width: 640px) + border: 0 none + @include border-radius(0) + margin: 0 + padding: 0 + .group - padding-bottom: 40px - hr - border-bottom: 10px solid white - outline: 0 - border-top: 0 - margin: 0 + padding-bottom: 0.5em + .row div + font-size: 110% + .row a + vertical-align: middle + .ofn-i_035-groups + font-size: 120% + vertical-align: middle + +// Individual Page +#group-page + .group-logo, .group-header + text-align: center + .group-logo + padding-bottom: 1em + max-height: 200px + .group-name + border-bottom: 1px solid #ccc + @media screen and (min-width: 768px) + .group-logo, .group-header + text-align: left + .group-logo + max-height: 120px + float: left + padding-right: 1em + background-color: white -.group-hero - position: relative - padding: 0 - border: 10px solid white - background: white + // Tabs + .tabs dd a // Mobile first + padding: 0.25rem 0.45rem 0rem + font-size: 0.75rem + border: none + margin-bottom: -2px + margin-right: 2px + text-transform: capitalize + @include avenir + @include border-radius(1em 0.25em 0 0) + @include gradient($disabled-light, $disabled-bright) + @media screen and (min-width: 768px) + .tabs dd a + padding: 0.5rem 1rem 0.25em + font-size: 0.875rem + @include border-radius(1.5em 0.25em 0 0) + @media screen and (min-width: 1024px) + .tabs dd a + padding: 0.75rem 1.5rem 0.5em + font-size: 1rem + @include border-radius(2em 0.25em 0 0) + .tabs dd.active a + @include gradient(white, white) + margin-bottom: -1px + border-top: 1px solid $light-grey + border-left: 1px solid $light-grey + border-right: 1px solid $light-grey + border-bottom: 0 + .tabs-content + border-top: 1px solid $light-grey + border-left: 1px solid $light-grey + border-right: 1px solid $light-grey + border-bottom: 1px solid $light-grey + padding: 1.5em -h3.group-name - margin-top: 0.5em - margin-bottom: 0.15em - -img.group-logo - max-width: 220px - max-height: 86px - float: right - padding-top: 10px - - -img.group-hero-img - background-color: black - width: 100% - height: inherit - max-height: 260px - min-height: 120px - overflow: hidden \ No newline at end of file + // Producers tab + .producers + background-image: none + .active_table .active_table_node a.is_distributor, .active_table .active_table_node a.is_distributor i.ofn-i_059-producer + color: $clr-turquoise + // Hubs tab + .hubs + background-image: none + padding-top: 0 + padding-bottom: 0 + \ No newline at end of file diff --git a/app/assets/stylesheets/darkswarm/map.css.sass b/app/assets/stylesheets/darkswarm/map.css.sass index acc280576c..a831e9448c 100644 --- a/app/assets/stylesheets/darkswarm/map.css.sass +++ b/app/assets/stylesheets/darkswarm/map.css.sass @@ -10,12 +10,12 @@ height: 100% width: 100% - img - // https://github.com/zurb/foundation/issues/112 + // https://github.com/zurb/foundation/issues/112 + img max-width: none height: auto - #pac-input + #pac-input @include big-input(#888, #333, $clr-brick) @include big-input-static font-size: 1.5rem diff --git a/app/assets/stylesheets/darkswarm/mixins.sass b/app/assets/stylesheets/darkswarm/mixins.sass index 00aa601b2f..6925e84f72 100644 --- a/app/assets/stylesheets/darkswarm/mixins.sass +++ b/app/assets/stylesheets/darkswarm/mixins.sass @@ -120,5 +120,21 @@ background-repeat: no-repeat background-size: 100% auto - +@mixin gradient($gradient-clr1, $gradient-clr2) + background: $gradient-clr1 + // Old browsers + background: -moz-linear-gradient(top, $gradient-clr1 0%, $gradient-clr2 100%) + // FF3.6+ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, $gradient-clr1), color-stop(100%, $gradient-clr2)) + // Chrome,Safari4+ + background: -webkit-linear-gradient(top, $gradient-clr1 0%, $gradient-clr2 100%) + // Chrome10+,Safari5.1+ + background: -o-linear-gradient(top, $gradient-clr1 0%, $gradient-clr2 100%) + // Opera 11.10+ + background: -ms-linear-gradient(top, $gradient-clr1 0%, $gradient-clr2 100%) + // IE10+ + background: linear-gradient(to bottom, $gradient-clr1 0%, $gradient-clr2 100%) + // W3C + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$gradient-clr1', endColorstr='$gradient-clr2',GradientType=0 ) + // IE6-8 diff --git a/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass b/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass index 305566ad58..e0090ca163 100644 --- a/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass +++ b/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass @@ -15,7 +15,7 @@ font-size: 1rem font-weight: 400 color: $disabled-dark - border-bottom: 1px solid $disabled-dark + border-bottom: 1px solid $light-grey margin-top: 0.75rem margin-bottom: 0.5rem @@ -67,8 +67,8 @@ margin-bottom: 0.5rem overflow-y: scroll overflow-x: hidden - border-bottom: 1px solid #999 - @include box-shadow(0 2px 2px -2px #999) + border-bottom: 1px solid $light-grey + @include box-shadow(0 2px 2px -2px $light-grey) .enterprise-logo, img float: left diff --git a/app/assets/stylesheets/darkswarm/shopping-cart.css.sass b/app/assets/stylesheets/darkswarm/shopping-cart.css.sass index 0cd0f20fac..cb71913831 100644 --- a/app/assets/stylesheets/darkswarm/shopping-cart.css.sass +++ b/app/assets/stylesheets/darkswarm/shopping-cart.css.sass @@ -42,10 +42,22 @@ height: auto top: 0px +// Shopping cart #cart-detail .cart-item-delete a.delete font-size: 1.125em + +.item-thumb-image + display: none + @media screen and (min-width: 640px) + display: inline-block + float: left + padding-right: 0.5em + width: 36px + height: 36px + + #edit-cart button, .button diff --git a/app/assets/stylesheets/darkswarm/typography.css.sass b/app/assets/stylesheets/darkswarm/typography.css.sass index eb1885d361..f549420d83 100644 --- a/app/assets/stylesheets/darkswarm/typography.css.sass +++ b/app/assets/stylesheets/darkswarm/typography.css.sass @@ -16,27 +16,45 @@ @font-face font-family: 'AvenirMed' src: url("/AvenirLTStd-Medium.otf") format("opentype") - -body - font-family: 'Open Sans', Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif + +$font-helvetica: "Helvetica Neue", "HelveticaNeue", "Helvetica", Helvetica, Arial, sans-serif a color: $clr-brick - &:hover + &:hover, &:focus, &:active text-decoration: none color: $clr-brick-bright +.text-big + font-size: 1.5rem + font-weight: 300 + small, .small font-size: 0.75rem .text-small font-size: 0.875rem margin-bottom: 0.5rem + font-family: $font-helvetica &, & * font-size: 0.875rem +.text-normal + font-weight: 400 + font-family: $font-helvetica + +.text-skinny + font-weight: 300 + font-family: $font-helvetica + .word-wrap word-wrap: break-word + +.pre-wrap + white-space: pre-wrap + +.pre-line + white-space: pre-line .light color: #999 @@ -81,6 +99,9 @@ ul.check-list .light-grey color: #666666 +.pad + padding: 1em + .pad-top padding-top: 1em diff --git a/app/assets/stylesheets/mail/email.css.sass b/app/assets/stylesheets/mail/email.css.sass index bc425a1e38..5f0a91bd72 100644 --- a/app/assets/stylesheets/mail/email.css.sass +++ b/app/assets/stylesheets/mail/email.css.sass @@ -56,6 +56,23 @@ table.social table.order-summary border-collapse: separate border-spacing: 0px 10px + tbody tr td + padding-left: 5px + padding-right: 5px + thead tr th + background-color: #f2f2f2 + border-bottom: 1px solid black + padding-left: 5px + padding-right: 5px + h4 + margin-top: 15px + tfoot + tr:first-child td + border-top: 1px solid black + padding-top: 5px + tr td + padding-left: 5px + padding-right: 5px .social .soc-btn padding: 3px 7px @@ -245,6 +262,10 @@ ul tr td padding: 15px +.pad + tr td + padding: 15px + .column-wrap padding: 0!important margin: 0 auto diff --git a/app/controllers/admin/enterprise_fees_controller.rb b/app/controllers/admin/enterprise_fees_controller.rb index 085828d7b3..b8ea46689f 100644 --- a/app/controllers/admin/enterprise_fees_controller.rb +++ b/app/controllers/admin/enterprise_fees_controller.rb @@ -20,6 +20,17 @@ module Admin end end + def for_order_cycle + respond_to do |format| + format.html + format.json do + render json: ActiveModel::ArraySerializer.new( @collection, + each_serializer: Api::Admin::EnterpriseFeeSerializer, controller: self + ).to_json + end + end + end + def bulk_update @enterprise_fee_set = EnterpriseFeeSet.new(params[:enterprise_fee_set]) if @enterprise_fee_set.save @@ -59,12 +70,26 @@ module Admin def load_data @calculators = EnterpriseFee.calculators.sort_by(&:name) + @tax_categories = Spree::TaxCategory.order('is_default DESC, name ASC') end def collection - collection = EnterpriseFee.managed_by(spree_current_user).order('enterprise_id', 'fee_type', 'name') - collection = collection.for_enterprise(current_enterprise) if current_enterprise - collection + case action + when :for_order_cycle + order_cycle = OrderCycle.find_by_id(params[:order_cycle_id]) if params[:order_cycle_id] + coordinator = Enterprise.find_by_id(params[:coordinator_id]) if params[:coordinator_id] + order_cycle = OrderCycle.new(coordinator: coordinator) if order_cycle.nil? && coordinator.present? + enterprises = OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user, order_cycle).visible_enterprises + return EnterpriseFee.for_enterprises(enterprises).order('enterprise_id', 'fee_type', 'name') + else + collection = EnterpriseFee.managed_by(spree_current_user).order('enterprise_id', 'fee_type', 'name') + collection = collection.for_enterprise(current_enterprise) if current_enterprise + collection + end + end + + def collection_actions + [:index, :for_order_cycle] end def current_enterprise diff --git a/app/controllers/admin/enterprise_groups_controller.rb b/app/controllers/admin/enterprise_groups_controller.rb index cc2f3ed1db..3f8888edfb 100644 --- a/app/controllers/admin/enterprise_groups_controller.rb +++ b/app/controllers/admin/enterprise_groups_controller.rb @@ -1,22 +1,50 @@ module Admin class EnterpriseGroupsController < ResourceController + before_filter :load_data, except: :index + before_filter :load_object_data, only: [:new, :edit, :create, :update] + def index + @enterprise_groups = @enterprise_groups.managed_by(spree_current_user) end def move_up - @enterprise_group = EnterpriseGroup.find params[:enterprise_group_id] - @enterprise_group.move_higher + EnterpriseGroup.with_isolation_level_serializable do + @enterprise_group = EnterpriseGroup.find params[:enterprise_group_id] + @enterprise_group.move_higher + end redirect_to main_app.admin_enterprise_groups_path end def move_down - @enterprise_group = EnterpriseGroup.find params[:enterprise_group_id] - @enterprise_group.move_lower + EnterpriseGroup.with_isolation_level_serializable do + @enterprise_group = EnterpriseGroup.find params[:enterprise_group_id] + @enterprise_group.move_lower + end redirect_to main_app.admin_enterprise_groups_path end + protected + + def build_resource_with_address + enterprise_group = build_resource_without_address + enterprise_group.address = Spree::Address.new + enterprise_group.address.country = Spree::Country.find_by_id(Spree::Config[:default_country_id]) + enterprise_group + end + alias_method_chain :build_resource, :address + private + def load_data + @countries = Spree::Country.order(:name) + @enterprises = Enterprise.activated + end + + def load_object_data + @owner_email = @enterprise_group.andand.owner.andand.email || "" + end + + def collection EnterpriseGroup.by_position end diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index aa8276020d..d4b3244525 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -7,17 +7,19 @@ module Admin before_filter :check_can_change_sells, only: :update before_filter :check_can_change_bulk_sells, only: :bulk_update before_filter :override_owner, only: :create + before_filter :override_sells, only: :create before_filter :check_can_change_owner, only: :update before_filter :check_can_change_bulk_owner, only: :bulk_update before_filter :check_can_change_managers, only: :update + before_filter :strip_new_properties, only: [:create, :update] + before_filter :load_properties, only: [:edit, :update] + before_filter :setup_property, only: [:edit] + helper 'spree/products' + include ActionView::Helpers::TextHelper include OrderCyclesHelper - def for_order_cycle - @collection = order_cycle_permitted_enterprises - end - def set_sells enterprise = Enterprise.find_by_permalink(params[:id]) || Enterprise.find(params[:id]) attributes = { sells: params[:sells] } @@ -45,17 +47,35 @@ module Admin def bulk_update @enterprise_set = EnterpriseSet.new(collection, params[:enterprise_set]) + touched_enterprises = @enterprise_set.collection.select(&:changed?) if @enterprise_set.save - flash[:success] = 'Enterprises updated successfully' + flash[:success] = "Enterprises updated successfully" + + # 18-3-2015: It seems that the form for this action sometimes loads bogus values for + # the 'sells' field, and submitting that form results in a bunch of enterprises with + # values that have mysteriously changed. This statement is here to help debug that + # issue, and should be removed (along with its display in index.html.haml) when the + # issue has been resolved. + flash[:action] = "Updated #{pluralize(touched_enterprises.count, 'enterprise')}: #{touched_enterprises.map(&:name).join(', ')}" + redirect_to main_app.admin_enterprises_path else - touched_ids = params[:enterprise_set][:collection_attributes].values.map { |v| v[:id].to_i } - @enterprise_set.collection.select! { |e| touched_ids.include? e.id } + @enterprise_set.collection.select! { |e| touched_enterprises.include? e } flash[:error] = 'Update failed' render :index end end + def for_order_cycle + respond_to do |format| + format.json do + render json: ActiveModel::ArraySerializer.new( @collection, + each_serializer: Api::Admin::ForOrderCycle::EnterpriseSerializer, spree_current_user: spree_current_user + ).to_json + end + end + end + protected def build_resource_with_address @@ -83,10 +103,18 @@ module Admin end def collection - # TODO was ordered with is_distributor DESC as well, not sure why or how we want to sort this now - OpenFoodNetwork::Permissions.new(spree_current_user). - editable_enterprises. - order('is_primary_producer ASC, name') + case action + when :for_order_cycle + order_cycle = OrderCycle.find_by_id(params[:order_cycle_id]) if params[:order_cycle_id] + coordinator = Enterprise.find_by_id(params[:coordinator_id]) if params[:coordinator_id] + order_cycle = OrderCycle.new(coordinator: coordinator) if order_cycle.nil? && coordinator.present? + return OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user, order_cycle).visible_enterprises + else + # TODO was ordered with is_distributor DESC as well, not sure why or how we want to sort this now + OpenFoodNetwork::Permissions.new(spree_current_user). + editable_enterprises. + order('is_primary_producer ASC, name') + end end def collection_actions @@ -119,6 +147,14 @@ module Admin params[:enterprise][:owner_id] = spree_current_user.id unless spree_current_user.admin? end + def override_sells + unless spree_current_user.admin? + has_hub = spree_current_user.owned_enterprises.is_hub.any? + new_enterprise_is_producer = Enterprise.new(params[:enterprise]).is_primary_producer + params[:enterprise][:sells] = (has_hub && !new_enterprise_is_producer) ? 'any' : 'none' + end + end + def check_can_change_owner unless ( spree_current_user == @enterprise.owner ) || spree_current_user.admin? params[:enterprise].delete :owner_id @@ -139,9 +175,27 @@ module Admin end end + def strip_new_properties + unless spree_current_user.admin? || params[:enterprise][:producer_properties_attributes].nil? + names = Spree::Property.pluck(:name) + params[:enterprise][:producer_properties_attributes].each do |key, property| + params[:enterprise][:producer_properties_attributes].delete key unless names.include? property[:property_name] + end + end + end + + def load_properties + @properties = Spree::Property.pluck(:name) + end + + def setup_property + @enterprise.producer_properties.build + end + # Overriding method on Spree's resource controller def location_after_save - if params[:enterprise].key? :producer_properties_attributes + refered_from_edit = URI(request.referer).path == main_app.edit_admin_enterprise_path(@enterprise) + if params[:enterprise].key?(:producer_properties_attributes) && !refered_from_edit main_app.admin_enterprises_path else main_app.edit_admin_enterprise_path(@enterprise) diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb index 63daf918de..fe1157a7f3 100644 --- a/app/controllers/admin/order_cycles_controller.rb +++ b/app/controllers/admin/order_cycles_controller.rb @@ -1,23 +1,32 @@ -require 'open_food_network/permissions' +require 'open_food_network/order_cycle_permissions' require 'open_food_network/order_cycle_form_applicator' module Admin class OrderCyclesController < ResourceController include OrderCyclesHelper - before_filter :load_order_cycle_set, :only => :index + before_filter :load_data_for_index, :only => :index + before_filter :require_coordinator, only: :new + before_filter :remove_protected_attrs, only: [:update] + before_filter :remove_unauthorized_bulk_attrs, only: [:bulk_update] + around_filter :protect_invalid_destroy, only: :destroy + def show respond_to do |format| format.html - format.json + format.json do + render json: Api::Admin::OrderCycleSerializer.new(@order_cycle, current_user: spree_current_user).to_json + end end end def new respond_to do |format| format.html - format.json + format.json do + render json: Api::Admin::OrderCycleSerializer.new(@order_cycle, current_user: spree_current_user).to_json + end end end @@ -26,7 +35,7 @@ module Admin respond_to do |format| if @order_cycle.save - OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, order_cycle_permitted_enterprises).go! + OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, spree_current_user).go! flash[:notice] = 'Your order cycle has been created.' format.html { redirect_to admin_order_cycles_path } @@ -43,7 +52,7 @@ module Admin respond_to do |format| if @order_cycle.update_attributes(params[:order_cycle]) - OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, order_cycle_permitted_enterprises).go! + OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, spree_current_user).go! flash[:notice] = 'Your order cycle has been updated.' format.html { redirect_to admin_order_cycles_path } @@ -72,18 +81,63 @@ module Admin protected - def collection - ocs = OrderCycle.managed_by(spree_current_user) + def collection(show_more=false) + ocs = OrderCycle.accessible_by(spree_current_user) ocs.undated + ocs.soonest_closing + ocs.soonest_opening + - ocs.most_recently_closed + (show_more ? ocs.closed : ocs.recently_closed) end private - def load_order_cycle_set - @order_cycle_set = OrderCycleSet.new :collection => collection + def load_data_for_index + @show_more = !!params[:show_more] + @order_cycle_set = OrderCycleSet.new :collection => collection(@show_more) + end + + def require_coordinator + if params[:coordinator_id] && @order_cycle.coordinator = permitted_coordinating_enterprises_for(@order_cycle).find_by_id(params[:coordinator_id]) + return + end + + available_coordinators = permitted_coordinating_enterprises_for(@order_cycle).select(&:confirmed?) + case available_coordinators.count + when 0 + flash[:error] = "None of your enterprises have permission to coordinate an order cycle" + redirect_to main_app.admin_order_cycles_path + when 1 + @order_cycle.coordinator = available_coordinators.first + else + flash[:error] = "You don't have permission to create an order cycle coordinated by that enterprise" if params[:coordinator_id] + render :set_coordinator + end + end + + def protect_invalid_destroy + begin + yield + rescue ActiveRecord::InvalidForeignKey + redirect_to main_app.admin_order_cycles_url + flash[:error] = "That order cycle has been selected by a customer and cannot be deleted. To prevent customers from accessing it, please close it instead." + end + end + + def remove_protected_attrs + params[:order_cycle].delete :coordinator_id + + unless Enterprise.managed_by(spree_current_user).include?(@order_cycle.coordinator) + params[:order_cycle].delete_if{ |k,v| [:name, :orders_open_at, :orders_close_at].include? k.to_sym } + end + end + + def remove_unauthorized_bulk_attrs + params[:order_cycle_set][:collection_attributes].each do |i, hash| + order_cycle = OrderCycle.find(hash[:id]) + unless Enterprise.managed_by(spree_current_user).include?(order_cycle.andand.coordinator) + params[:order_cycle_set][:collection_attributes].delete i + end + end end end end diff --git a/app/controllers/admin/variant_overrides_controller.rb b/app/controllers/admin/variant_overrides_controller.rb index 234c2779c4..9425565f6e 100644 --- a/app/controllers/admin/variant_overrides_controller.rb +++ b/app/controllers/admin/variant_overrides_controller.rb @@ -2,27 +2,18 @@ require 'open_food_network/spree_api_key_loader' module Admin class VariantOverridesController < ResourceController - include OrderCyclesHelper include OpenFoodNetwork::SpreeApiKeyLoader before_filter :load_spree_api_key, only: :index + before_filter :load_data def index - @hubs = order_cycle_hub_enterprises(without_validation: true) - - # Used in JS to look up the name of the producer of each product - @producers = OpenFoodNetwork::Permissions.new(spree_current_user). - variant_override_producers - - @hub_permissions = OpenFoodNetwork::Permissions.new(spree_current_user). - variant_override_enterprises_per_hub - @variant_overrides = VariantOverride.for_hubs(@hubs) end def bulk_update collection_hash = Hash[params[:variant_overrides].each_with_index.map { |vo, i| [i, vo] }] - vo_set = VariantOverrideSet.new collection_attributes: collection_hash + vo_set = VariantOverrideSet.new @variant_overrides, collection_attributes: collection_hash # Ensure we're authorised to update all variant overrides vo_set.collection.each { |vo| authorize! :update, vo } @@ -40,6 +31,21 @@ module Admin end + private + + def load_data + @hubs = OpenFoodNetwork::Permissions.new(spree_current_user). + variant_override_hubs.by_name + + # Used in JS to look up the name of the producer of each product + @producers = OpenFoodNetwork::Permissions.new(spree_current_user). + variant_override_producers + + @hub_permissions = OpenFoodNetwork::Permissions.new(spree_current_user). + variant_override_enterprises_per_hub + @variant_overrides = VariantOverride.for_hubs(@hubs) + end + def collection end end diff --git a/app/controllers/api/enterprises_controller.rb b/app/controllers/api/enterprises_controller.rb index d40d67409d..468f3a22ad 100644 --- a/app/controllers/api/enterprises_controller.rb +++ b/app/controllers/api/enterprises_controller.rb @@ -63,7 +63,9 @@ module Api end def override_sells - params[:enterprise][:sells] = 'unspecified' + has_hub = current_api_user.owned_enterprises.is_hub.any? + new_enterprise_is_producer = !!params[:enterprise][:is_primary_producer] + params[:enterprise][:sells] = (has_hub && !new_enterprise_is_producer) ? 'any' : 'unspecified' end def override_visible diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb index 024c250ede..0c42ceb987 100644 --- a/app/controllers/checkout_controller.rb +++ b/app/controllers/checkout_controller.rb @@ -19,6 +19,7 @@ class CheckoutController < Spree::CheckoutController def update if @order.update_attributes(object_params) + check_order_for_phantom_fees fire_event('spree.checkout.update') while @order.state != "complete" if @order.state == "payment" @@ -58,6 +59,20 @@ class CheckoutController < Spree::CheckoutController private + def check_order_for_phantom_fees + phantom_fees = @order.adjustments.joins('LEFT OUTER JOIN spree_line_items ON spree_line_items.id = spree_adjustments.source_id'). + where("originator_type = 'EnterpriseFee' AND source_type = 'Spree::LineItem' AND spree_line_items.id IS NULL") + + if phantom_fees.any? + Bugsnag.notify(RuntimeError.new("Phantom Fees"), { + phantom_fees: { + phantom_total: phantom_fees.sum(&:amount).to_s, + phantom_fees: phantom_fees.as_json + } + }) + end + end + # Copied and modified from spree. Remove check for order state, since the state machine is # progressed all the way in one go with the one page checkout. def object_params @@ -110,7 +125,7 @@ class CheckoutController < Spree::CheckoutController 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 || Spree::Address.default end def after_payment diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 504bfa9569..8653131b5a 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -5,4 +5,8 @@ class GroupsController < BaseController def index @groups = EnterpriseGroup.on_front_page.by_position end + + def show + @group = EnterpriseGroup.find params[:id] + end end diff --git a/app/controllers/spree/admin/products_controller_decorator.rb b/app/controllers/spree/admin/products_controller_decorator.rb index c0e1f8b948..1591586f76 100644 --- a/app/controllers/spree/admin/products_controller_decorator.rb +++ b/app/controllers/spree/admin/products_controller_decorator.rb @@ -5,6 +5,7 @@ Spree::Admin::ProductsController.class_eval do include OrderCyclesHelper before_filter :load_form_data, :only => [:bulk_edit, :new, :create, :edit, :update] before_filter :load_spree_api_key, :only => [:bulk_edit, :variant_overrides] + before_filter :strip_new_properties, only: [:create, :update] alias_method :location_after_save_original, :location_after_save @@ -95,4 +96,13 @@ Spree::Admin::ProductsController.class_eval do @producers = OpenFoodNetwork::Permissions.new(spree_current_user).managed_product_enterprises.is_primary_producer.by_name @taxons = Spree::Taxon.order(:name) end + + def strip_new_properties + unless spree_current_user.admin? || params[:product][:product_properties_attributes].nil? + names = Spree::Property.pluck(:name) + params[:product][:product_properties_attributes].each do |key, property| + params[:product][:product_properties_attributes].delete key unless names.include? property[:property_name] + end + end + end end diff --git a/app/controllers/spree/admin/reports_controller_decorator.rb b/app/controllers/spree/admin/reports_controller_decorator.rb index 655d1778dc..f148456813 100644 --- a/app/controllers/spree/admin/reports_controller_decorator.rb +++ b/app/controllers/spree/admin/reports_controller_decorator.rb @@ -6,6 +6,7 @@ require 'open_food_network/order_grouper' require 'open_food_network/customers_report' require 'open_food_network/users_and_enterprises_report' require 'open_food_network/order_cycle_management_report' +require 'open_food_network/sales_tax_report' Spree::Admin::ReportsController.class_eval do @@ -27,7 +28,8 @@ Spree::Admin::ReportsController.class_eval do ["Addresses", :addresses] ], order_cycle_management: [ - ["Payment Methods Report", :payment_methods_report] + ["Payment Methods Report", :payment_methods], + ["Delivery Report", :delivery] ] } @@ -58,7 +60,6 @@ Spree::Admin::ReportsController.class_eval do @report_types = REPORT_TYPES[:customers] @report_type = params[:report_type] @report = OpenFoodNetwork::CustomersReport.new spree_current_user, params - render_report(@report.header, @report.table, params[:csv], "customers_#{timestamp}.csv") end @@ -68,14 +69,13 @@ Spree::Admin::ReportsController.class_eval do @report = OpenFoodNetwork::OrderCycleManagementReport.new spree_current_user, params @search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q]) - @orders = @search.result render_report(@report.header, @report.table, params[:csv], "order_cycle_management_#{timestamp}.csv") end def orders_and_distributors - params[:q] = {} unless params[:q] + params[:q] ||= {} if params[:q][:completed_at_gt].blank? params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month @@ -103,8 +103,38 @@ Spree::Admin::ReportsController.class_eval do end end + def sales_tax + params[:q] ||= {} + + if params[:q][:completed_at_gt].blank? + params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month + else + params[:q][:completed_at_gt] = Time.zone.parse(params[:q][:completed_at_gt]).beginning_of_day rescue Time.zone.now.beginning_of_month + end + + if params[:q] && !params[:q][:completed_at_lt].blank? + params[:q][:completed_at_lt] = Time.zone.parse(params[:q][:completed_at_lt]).end_of_day rescue "" + end + params[:q][:meta_sort] ||= "completed_at.desc" + + @search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q]) + orders = @search.result + @distributors = Enterprise.is_distributor.managed_by(spree_current_user) + + @report = OpenFoodNetwork::SalesTaxReport.new orders + unless params[:csv] + render :html => @report + else + csv_string = CSV.generate do |csv| + csv << @report.header + @report.table.each { |row| csv << row } + end + send_data csv_string, :filename => "sales_tax.csv" + end + end + def bulk_coop - params[:q] = {} unless params[:q] + params[:q] ||= {} if params[:q][:completed_at_gt].blank? params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month @@ -257,7 +287,7 @@ Spree::Admin::ReportsController.class_eval do end def payments - params[:q] = {} unless params[:q] + params[:q] ||= {} if params[:q][:completed_at_gt].blank? params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month @@ -495,18 +525,26 @@ Spree::Admin::ReportsController.class_eval do table_items = @line_items @include_blank = 'All' - header = ["Hub", "Customer", "Email", "Phone", "Producer", "Product", "Variant", "Amount", "Item (#{currency_symbol})", "Item + Fees (#{currency_symbol})", "Dist (#{currency_symbol})", "Ship (#{currency_symbol})", "Total (#{currency_symbol})", "Paid?", - "Shipping", "Delivery?", "Ship street", "Ship street 2", "Ship city", "Ship postcode", "Ship state", "Order notes"] + header = ["Hub", "Customer", "Email", "Phone", "Producer", "Product", "Variant", + "Amount", "Item (#{currency_symbol})", "Item + Fees (#{currency_symbol})", "Admin & Handling (#{currency_symbol})", "Ship (#{currency_symbol})", "Total (#{currency_symbol})", "Paid?", + "Shipping", "Delivery?", + "Ship Street", "Ship Street 2", "Ship City", "Ship Postcode", "Ship State", + "Comments", "SKU", + "Order Cycle", "Payment Method", "Customer Code", "Tags", + "Billing Street 1", "Billing Street 2", "Billing City", "Billing Postcode", "Billing State" + ] rsa = proc { |line_items| line_items.first.order.shipping_method.andand.require_ship_address } - columns = [ proc { |line_items| line_items.first.order.distributor.name }, + columns = [ + proc { |line_items| line_items.first.order.distributor.name }, proc { |line_items| line_items.first.order.bill_address.firstname + " " + line_items.first.order.bill_address.lastname }, proc { |line_items| line_items.first.order.email }, proc { |line_items| line_items.first.order.bill_address.phone }, proc { |line_items| line_items.first.variant.product.supplier.name }, proc { |line_items| line_items.first.variant.product.name }, proc { |line_items| line_items.first.variant.full_name }, + proc { |line_items| line_items.sum { |li| li.quantity } }, proc { |line_items| line_items.sum { |li| li.amount } }, proc { |line_items| line_items.sum { |li| li.amount_with_adjustments } }, @@ -517,25 +555,40 @@ Spree::Admin::ReportsController.class_eval do proc { |line_items| line_items.first.order.shipping_method.andand.name }, proc { |line_items| rsa.call(line_items) ? 'Y' : 'N' }, + proc { |line_items| line_items.first.order.ship_address.andand.address1 if rsa.call(line_items) }, proc { |line_items| line_items.first.order.ship_address.andand.address2 if rsa.call(line_items) }, proc { |line_items| line_items.first.order.ship_address.andand.city if rsa.call(line_items) }, proc { |line_items| line_items.first.order.ship_address.andand.zipcode if rsa.call(line_items) }, proc { |line_items| line_items.first.order.ship_address.andand.state if rsa.call(line_items) }, - proc { |line_items| line_items.first.order.special_instructions }] + proc { |line_items| "" }, + proc { |line_items| line_items.first.variant.product.sku }, + + proc { |line_items| line_items.first.order.order_cycle.andand.name }, + proc { |line_items| line_items.first.order.payments.first.andand.payment_method.andand.name }, + proc { |line_items| line_items.first.order.user.andand.customer_of(line_items.first.order.distributor).andand.code }, + proc { |line_items| "" }, + + proc { |line_items| line_items.first.order.bill_address.andand.address1 }, + proc { |line_items| line_items.first.order.bill_address.andand.address2 }, + proc { |line_items| line_items.first.order.bill_address.andand.city }, + proc { |line_items| line_items.first.order.bill_address.andand.zipcode }, + proc { |line_items| line_items.first.order.bill_address.andand.state } ] rules = [ { group_by: proc { |line_item| line_item.order.distributor }, sort_by: proc { |distributor| distributor.name } }, { group_by: proc { |line_item| line_item.order }, sort_by: proc { |order| order.bill_address.lastname + " " + order.bill_address.firstname }, - summary_columns: [ proc { |line_items| line_items.first.order.distributor.name }, + summary_columns: [ + proc { |line_items| line_items.first.order.distributor.name }, proc { |line_items| line_items.first.order.bill_address.firstname + " " + line_items.first.order.bill_address.lastname }, proc { |line_items| "" }, proc { |line_items| "" }, proc { |line_items| "" }, proc { |line_items| "TOTAL" }, proc { |line_items| "" }, + proc { |line_items| "" }, proc { |line_items| line_items.sum { |li| li.amount } }, proc { |line_items| line_items.sum { |li| li.amount_with_adjustments } }, @@ -546,13 +599,27 @@ Spree::Admin::ReportsController.class_eval do proc { |line_items| "" }, proc { |line_items| "" }, + proc { |line_items| "" }, proc { |line_items| "" }, proc { |line_items| "" }, proc { |line_items| "" }, proc { |line_items| "" }, - proc { |line_items| "" } ] }, + proc { |line_items| line_items.first.order.special_instructions } , + proc { |line_items| "" }, + + proc { |line_items| line_items.first.order.order_cycle.andand.name }, + proc { |line_items| line_items.first.order.payments.first.andand.payment_method.andand.name }, + proc { |line_items| "" }, + proc { |line_items| "" }, + + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" }, + proc { |line_items| "" } + ] }, { group_by: proc { |line_item| line_item.variant.product }, sort_by: proc { |product| product.name } }, @@ -641,7 +708,8 @@ Spree::Admin::ReportsController.class_eval do :products_and_inventory => {:name => "Products & Inventory", :description => ''}, :sales_total => { :name => "Sales Total", :description => "Sales Total For All Orders" }, :users_and_enterprises => { :name => "Users & Enterprises", :description => "Enterprise Ownership & Status" }, - :order_cycle_management => {:name => "Order Cycle Management", :description => ''} + :order_cycle_management => {:name => "Order Cycle Management", :description => ''}, + :sales_tax => { :name => "Sales Tax", :description => "Sales Tax For Orders" } } # Return only reports the user is authorized to view. reports.select { |action| can? action, :report } diff --git a/app/controllers/spree/api/products_controller_decorator.rb b/app/controllers/spree/api/products_controller_decorator.rb index af7834bb04..0186c6e8df 100644 --- a/app/controllers/spree/api/products_controller_decorator.rb +++ b/app/controllers/spree/api/products_controller_decorator.rb @@ -20,15 +20,6 @@ Spree::Api::ProductsController.class_eval do render_paged_products @products end - def distributable - producers = OpenFoodNetwork::Permissions.new(current_api_user). - order_cycle_enterprises.is_primary_producer.by_name - - @products = paged_products_for_producers producers - - render_paged_products @products - end - def overridable producers = OpenFoodNetwork::Permissions.new(current_api_user). variant_override_producers.by_name diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 50b9aa1125..14c4f9c20d 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -62,6 +62,10 @@ module Admin admin_inject_json_ams_array "ofn.admin", "variantOverrides", @variant_overrides, Api::Admin::VariantOverrideSerializer end + def admin_inject_order_cycle_instance + render partial: "admin/json/injection_ams", locals: {ngModule: 'admin.order_cycles', name: 'ocInstance', json: "{coordinator_id: '#{@order_cycle.coordinator.id}'}"} + end + def admin_inject_spree_api_key render partial: "admin/json/injection_ams", locals: {ngModule: 'ofn.admin', name: 'SpreeApiKey', json: "'#{@spree_api_key.to_s}'"} end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index c091b2fc82..c5de5c1ca0 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -1,2 +1,23 @@ module GroupsHelper + + def link_to_service(baseurl, name, html_options = {}) + return if name.blank? + html_options = html_options.merge target: '_blank' + link_to ext_url(baseurl, name), html_options do + yield + end + end + + def ext_url(prefix, url) + if url =~ /^https?:\/\//i + url + else + prefix + url + end + end + + def strip_url(url) + url.andand.sub(/^https?:\/\//i, '') + end + end diff --git a/app/helpers/order_cycles_helper.rb b/app/helpers/order_cycles_helper.rb index 3868de95c0..18222ee1f0 100644 --- a/app/helpers/order_cycles_helper.rb +++ b/app/helpers/order_cycles_helper.rb @@ -3,41 +3,32 @@ module OrderCyclesHelper @current_order_cycle ||= current_order(false).andand.order_cycle end - def order_cycle_permitted_enterprises - OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_enterprises + def permitted_enterprises_for(order_cycle) + OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user, order_cycle).visible_enterprises end - def order_cycle_producer_enterprises - order_cycle_permitted_enterprises.is_primary_producer.by_name + def permitted_producer_enterprises_for(order_cycle) + permitted_enterprises_for(order_cycle).is_primary_producer.by_name end - def order_cycle_coordinating_enterprises - order_cycle_permitted_enterprises.is_distributor.by_name + def permitted_producer_enterprise_options_for(order_cycle) + validated_enterprise_options permitted_producer_enterprises_for(order_cycle), confirmed: true end - def order_cycle_hub_enterprises(options={}) - enterprises = order_cycle_permitted_enterprises.is_distributor.by_name + def permitted_coordinating_enterprises_for(order_cycle) + Enterprise.managed_by(spree_current_user).is_distributor.by_name + end - if options[:without_validation] - enterprises - else - enterprises.map do |e| - disabled_message = nil - if e.shipping_methods.empty? && e.payment_methods.available.empty? - disabled_message = 'no shipping or payment methods' - elsif e.shipping_methods.empty? - disabled_message = 'no shipping methods' - elsif e.payment_methods.available.empty? - disabled_message = 'no payment methods' - end + def permitted_coordinating_enterprise_options_for(order_cycle) + validated_enterprise_options permitted_coordinating_enterprises_for(order_cycle), confirmed: true + end - if disabled_message - ["#{e.name} (#{disabled_message})", e.id, {disabled: true}] - else - [e.name, e.id] - end - end - end + def permitted_hub_enterprises_for(order_cycle) + permitted_enterprises_for(order_cycle).is_hub.by_name + end + + def permitted_hub_enterprise_options_for(order_cycle) + validated_enterprise_options permitted_hub_enterprises_for(order_cycle), confirmed: true, shipping_and_payment_methods: true end def order_cycle_status_class(order_cycle) @@ -68,8 +59,12 @@ module OrderCyclesHelper OrderCycle.active.with_distributor(@distributor).present? end - def order_cycles_simple_view - @order_cycles_simple_view ||= !OpenFoodNetwork::Permissions.new(spree_current_user).can_manage_complex_order_cycles? + def order_cycles_simple_index + @order_cycles_simple_index ||= !OpenFoodNetwork::Permissions.new(spree_current_user).can_manage_complex_order_cycles? + end + + def order_cycles_simple_form + @order_cycles_simple_form ||= @order_cycle.coordinator.sells == 'own' end def order_cycles_enabled? @@ -80,4 +75,36 @@ module OrderCyclesHelper order_cycle.exchanges.to_enterprises(current_distributor).outgoing.first.pickup_time end + def can_delete?(order_cycle) + Spree::Order.where(order_cycle_id: order_cycle).none? + end + + def viewing_as_coordinator_of?(order_cycle) + Enterprise.managed_by(spree_current_user).include? order_cycle.coordinator + end + + private + + def validated_enterprise_options(enterprises, options={}) + enterprises.map do |e| + disabled_message = nil + if options[:shipping_and_payment_methods] && (e.shipping_methods.empty? || e.payment_methods.available.empty?) + if e.shipping_methods.empty? && e.payment_methods.available.empty? + disabled_message = 'no shipping or payment methods' + elsif e.shipping_methods.empty? + disabled_message = 'no shipping methods' + elsif e.payment_methods.available.empty? + disabled_message = 'no payment methods' + end + elsif options[:confirmed] && !e.confirmed? + disabled_message = 'unconfirmed' + end + + if disabled_message + ["#{e.name} (#{disabled_message})", e.id, {disabled: true}] + else + [e.name, e.id] + end + end + end end diff --git a/app/jobs/confirm_order_job.rb b/app/jobs/confirm_order_job.rb new file mode 100644 index 0000000000..e16df2d99a --- /dev/null +++ b/app/jobs/confirm_order_job.rb @@ -0,0 +1,6 @@ +ConfirmOrderJob = Struct.new(:order_id) do + def perform + Spree::OrderMailer.confirm_email_for_customer(order_id).deliver + Spree::OrderMailer.confirm_email_for_shop(order_id).deliver + end +end diff --git a/app/jobs/confirm_signup_job.rb b/app/jobs/confirm_signup_job.rb new file mode 100644 index 0000000000..d16aee714c --- /dev/null +++ b/app/jobs/confirm_signup_job.rb @@ -0,0 +1,6 @@ +ConfirmSignupJob = Struct.new(:user_id) do + def perform + user = Spree::User.find user_id + Spree::UserMailer.signup_confirmation(user).deliver + end +end diff --git a/app/jobs/welcome_enterprise_job.rb b/app/jobs/welcome_enterprise_job.rb new file mode 100644 index 0000000000..6665af9d5a --- /dev/null +++ b/app/jobs/welcome_enterprise_job.rb @@ -0,0 +1,6 @@ +WelcomeEnterpriseJob = Struct.new(:enterprise_id) do + def perform + enterprise = Enterprise.find enterprise_id + EnterpriseMailer.welcome(enterprise).deliver + end +end diff --git a/app/models/customer.rb b/app/models/customer.rb new file mode 100644 index 0000000000..d3fa9e093f --- /dev/null +++ b/app/models/customer.rb @@ -0,0 +1,10 @@ +class Customer < ActiveRecord::Base + belongs_to :enterprise + belongs_to :user, :class_name => Spree.user_class + + validates :code, presence: true, uniqueness: {scope: :enterprise_id} + validates :email, presence: true + validates :enterprise_id, presence: true + + scope :of, ->(enterprise) { where(enterprise_id: enterprise) } +end diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 8f5c940e77..97766078b1 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -15,6 +15,7 @@ class Enterprise < ActiveRecord::Base has_and_belongs_to_many :groups, class_name: 'EnterpriseGroup' has_many :producer_properties, foreign_key: 'producer_id' + has_many :properties, through: :producer_properties has_many :supplied_products, :class_name => 'Spree::Product', :foreign_key => 'supplier_id', :dependent => :destroy has_many :distributed_orders, :class_name => 'Spree::Order', :foreign_key => 'distributor_id' belongs_to :address, :class_name => 'Spree::Address' @@ -114,6 +115,9 @@ class Enterprise < ActiveRecord::Base scope :with_distributed_products_outer, joins('LEFT OUTER JOIN product_distributions ON product_distributions.distributor_id = enterprises.id'). joins('LEFT OUTER JOIN spree_products ON spree_products.id = product_distributions.product_id') + scope :with_order_cycles_as_supplier_outer, + joins("LEFT OUTER JOIN exchanges ON (exchanges.sender_id = enterprises.id AND exchanges.incoming = 't')"). + joins('LEFT OUTER JOIN order_cycles ON (order_cycles.id = exchanges.order_cycle_id)') scope :with_order_cycles_as_distributor_outer, joins("LEFT OUTER JOIN exchanges ON (exchanges.receiver_id = enterprises.id AND exchanges.incoming = 'f')"). joins('LEFT OUTER JOIN order_cycles ON (order_cycles.id = exchanges.order_cycle_id)') @@ -253,6 +257,10 @@ class Enterprise < ActiveRecord::Base self.sells != "none" end + def is_hub + self.sells == 'any' + end + # Simplify enterprise categories for frontend logic and icons, and maybe other things. def category # Make this crazy logic human readable so we can argue about it sanely. @@ -340,7 +348,7 @@ class Enterprise < ActiveRecord::Base end def send_welcome_email - EnterpriseMailer.welcome(self).deliver + Delayed::Job.enqueue WelcomeEnterpriseJob.new(self.id) end def strip_url(url) @@ -366,26 +374,30 @@ class Enterprise < ActiveRecord::Base end def relate_to_owners_enterprises - # When a new enterprise is created, we relate them to all enterprises owned by - # the same owner, in both directions. So all enterprises owned by the same owner - # will have permissions to every other one, in both directions. + # When a new producer is created, it grants permissions to all pre-existing hubs + # When a new hub is created, + # - it grants permissions to all pre-existing hubs + # - all producers grant permission to it enterprises = owner.owned_enterprises.where('enterprises.id != ?', self) - enterprises.each do |enterprise| + # We grant permissions to all pre-existing hubs + hub_permissions = [:add_to_order_cycle] + hub_permissions << :create_variant_overrides if is_primary_producer + enterprises.is_hub.each do |enterprise| EnterpriseRelationship.create!(parent: self, child: enterprise, - permissions_list: [:add_to_order_cycle, - :manage_products, - :edit_profile, - :create_variant_overrides]) + permissions_list: hub_permissions) + end - EnterpriseRelationship.create!(parent: enterprise, - child: self, - permissions_list: [:add_to_order_cycle, - :manage_products, - :edit_profile, - :create_variant_overrides]) + # All pre-existing producers grant permission to new hubs + if is_hub + enterprises.is_primary_producer.each do |enterprise| + EnterpriseRelationship.create!(parent: enterprise, + child: self, + permissions_list: [:add_to_order_cycle, + :create_variant_overrides]) + end end end diff --git a/app/models/enterprise_fee.rb b/app/models/enterprise_fee.rb index 6270f77b7a..e19f91d0f4 100644 --- a/app/models/enterprise_fee.rb +++ b/app/models/enterprise_fee.rb @@ -1,5 +1,6 @@ class EnterpriseFee < ActiveRecord::Base belongs_to :enterprise + belongs_to :tax_category, class_name: 'Spree::TaxCategory', foreign_key: 'tax_category_id' has_and_belongs_to_many :order_cycles, join_table: 'coordinator_fees' has_many :exchange_fees, dependent: :destroy has_many :exchanges, through: :exchange_fees @@ -8,7 +9,7 @@ class EnterpriseFee < ActiveRecord::Base calculated_adjustments - attr_accessible :enterprise_id, :fee_type, :name, :calculator_type + attr_accessible :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type FEE_TYPES = %w(packing transport admin sales fundraising) PER_ORDER_CALCULATORS = ['Spree::Calculator::FlatRate', 'Spree::Calculator::FlexiRate'] @@ -19,6 +20,7 @@ class EnterpriseFee < ActiveRecord::Base scope :for_enterprise, lambda { |enterprise| where(enterprise_id: enterprise) } + scope :for_enterprises, lambda { |enterprises| where(enterprise_id: enterprises) } scope :managed_by, lambda { |user| if user.has_spree_role?('admin') diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb index 6e61b1a564..9d510d8f2b 100644 --- a/app/models/enterprise_group.rb +++ b/app/models/enterprise_group.rb @@ -1,13 +1,28 @@ +require 'open_food_network/locking' + class EnterpriseGroup < ActiveRecord::Base acts_as_list has_and_belongs_to_many :enterprises + belongs_to :owner, class_name: 'Spree::User', foreign_key: :owner_id, inverse_of: :owned_groups + belongs_to :address, :class_name => 'Spree::Address' + accepts_nested_attributes_for :address + validates :address, presence: true, associated: true + before_validation :set_undefined_address_fields + before_validation :set_unused_address_fields + after_find :unset_undefined_address_fields + after_save :unset_undefined_address_fields validates :name, presence: true validates :description, presence: true attr_accessible :name, :description, :long_description, :on_front_page, :enterprise_ids + attr_accessible :owner_id attr_accessible :logo, :promo_image + attr_accessible :address_attributes + attr_accessible :email, :website, :facebook, :instagram, :linkedin, :twitter + + delegate :phone, :address1, :address2, :city, :zipcode, :state, :country, :to => :address has_attached_file :logo, styles: {medium: "100x100"}, @@ -28,4 +43,32 @@ class EnterpriseGroup < ActiveRecord::Base scope :by_position, order('position ASC') scope :on_front_page, where(on_front_page: true) + scope :managed_by, lambda { |user| + if user.has_spree_role?('admin') + scoped + else + where('owner_id = ?', user.id) + end + } + + def set_unused_address_fields + address.firstname = address.lastname = 'unused' if address.present? + end + + def set_undefined_address_fields + return unless address.present? + address.phone.present? || address.phone = 'undefined' + address.address1.present? || address.address1 = 'undefined' + address.city.present? || address.city = 'undefined' + address.zipcode.present? || address.zipcode = 'undefined' + end + + def unset_undefined_address_fields + return unless address.present? + address.phone.sub!(/^undefined$/, '') + address.address1.sub!(/^undefined$/, '') + address.city.sub!(/^undefined$/, '') + address.zipcode.sub!(/^undefined$/, '') + end + end diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index 600ef87f11..fbdef9d52c 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -28,4 +28,8 @@ class EnterpriseRelationship < ActiveRecord::Base def permissions_list=(perms) perms.andand.each { |name| permissions.build name: name } end + + def has_permission?(name) + permissions.map(&:name).map(&:to_sym).include? name.to_sym + end end diff --git a/app/models/exchange.rb b/app/models/exchange.rb index c491748d5d..1b226d269a 100644 --- a/app/models/exchange.rb +++ b/app/models/exchange.rb @@ -22,6 +22,7 @@ class Exchange < ActiveRecord::Base scope :to_enterprise, lambda { |enterprise| where(receiver_id: enterprise) } scope :from_enterprises, lambda { |enterprises| where('exchanges.sender_id IN (?)', enterprises) } scope :to_enterprises, lambda { |enterprises| where('exchanges.receiver_id IN (?)', enterprises) } + scope :involving, lambda { |enterprises| where('exchanges.receiver_id IN (?) OR exchanges.sender_id IN (?)', enterprises, enterprises).select('DISTINCT exchanges.*') } scope :supplying_to, lambda { |distributor| where('exchanges.incoming OR exchanges.receiver_id = ?', distributor) } scope :with_variant, lambda { |variant| joins(:exchange_variants).where('exchange_variants.variant_id = ?', variant) } scope :with_any_variant, lambda { |variants| joins(:exchange_variants).where('exchange_variants.variant_id IN (?)', variants).select('DISTINCT exchanges.*') } diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index 134af04c91..adcd596a8c 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -19,7 +19,14 @@ class OrderCycle < ActiveRecord::Base scope :undated, where(orders_open_at: nil, orders_close_at: nil) scope :soonest_closing, lambda { active.order('order_cycles.orders_close_at ASC') } + # TODO This method returns all the closed orders. So maybe we can replace it with :recently_closed. scope :most_recently_closed, lambda { closed.order('order_cycles.orders_close_at DESC') } + + scope :recently_closed, -> { + closed. + where("order_cycles.orders_close_at >= ?", 31.days.ago). + order("order_cycles.orders_close_at DESC") } + scope :soonest_opening, lambda { upcoming.order('order_cycles.orders_open_at ASC') } scope :distributing_product, lambda { |product| diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index eb8dcbd60d..e66c03aa72 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -6,7 +6,9 @@ class AbilityDecorator def initialize(user) add_base_abilities user if is_new_user? user add_enterprise_management_abilities user if can_manage_enterprises? user + add_group_management_abilities user if can_manage_groups? user add_product_management_abilities user if can_manage_products? user + add_order_cycle_management_abilities user if can_manage_order_cycles? user add_order_management_abilities user if can_manage_orders? user add_relationship_management_abilities user if can_manage_relationships? user end @@ -21,12 +23,24 @@ class AbilityDecorator user.enterprises.present? end + # Users can manage a group if they have one. + def can_manage_groups?(user) + user.owned_groups.present? + end + # Users can manage products if they have an enterprise that is not a profile. def can_manage_products?(user) can_manage_enterprises?(user) && user.enterprises.any? { |e| e.category != :hub_profile && e.producer_profile_only != true } end + # Users can manage order cycles if they manage a sells own/any enterprise + # OR if they manage a producer which is included in any order cycles + def can_manage_order_cycles?(user) + can_manage_orders?(user) || + OrderCycle.accessible_by(user).any? + end + # Users can manage orders if they have a sells own/any enterprise. def can_manage_orders?(user) ( user.enterprises.map(&:sells) & %w(own any) ).any? @@ -41,6 +55,14 @@ class AbilityDecorator can [:create], Enterprise end + def add_group_management_abilities(user) + can [:admin, :index], :overview + can [:admin, :index], EnterpriseGroup + can [:read, :edit, :update], EnterpriseGroup do |group| + user.owned_groups.include? group + end + end + def add_enterprise_management_abilities(user) # Spree performs authorize! on (:create, nil) when creating a new order from admin, and also (:search, nil) # when searching for variants to add to the order @@ -81,7 +103,7 @@ class AbilityDecorator can [:admin, :index, :read, :update, :bulk_update], VariantOverride do |vo| hub_auth = OpenFoodNetwork::Permissions.new(user). - order_cycle_enterprises.is_distributor. + variant_override_hubs. include? vo.hub producer_auth = OpenFoodNetwork::Permissions.new(user). @@ -101,6 +123,17 @@ class AbilityDecorator can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory], :report end + def add_order_cycle_management_abilities(user) + can [:admin, :index, :read, :edit, :update], OrderCycle do |order_cycle| + OrderCycle.accessible_by(user).include? order_cycle + end + can [:bulk_update, :clone, :destroy], OrderCycle do |order_cycle| + user.enterprises.include? order_cycle.coordinator + end + can [:for_order_cycle], Enterprise + can [:for_order_cycle], EnterpriseFee + end + def add_order_management_abilities(user) # Enterprise User can only access orders that they are a distributor for can [:index, :create], Spree::Order @@ -118,10 +151,6 @@ class AbilityDecorator can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::ReturnAuthorization can [:create], OrderCycle - can [:admin, :index, :read, :edit, :update, :bulk_update, :clone], OrderCycle do |order_cycle| - user.enterprises.include? order_cycle.coordinator - end - can [:for_order_cycle], Enterprise can [:admin, :index, :read, :create, :edit, :update], ExchangeVariant can [:admin, :index, :read, :create, :edit, :update], Exchange @@ -139,7 +168,7 @@ class AbilityDecorator end # Reports page - can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report + can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report end diff --git a/app/models/spree/adjustment_decorator.rb b/app/models/spree/adjustment_decorator.rb index 2c5bb0be0a..836080183c 100644 --- a/app/models/spree/adjustment_decorator.rb +++ b/app/models/spree/adjustment_decorator.rb @@ -3,5 +3,17 @@ module Spree has_one :metadata, class_name: 'AdjustmentMetadata', dependent: :destroy scope :enterprise_fee, where(originator_type: 'EnterpriseFee') + scope :included_tax, where(originator_type: 'Spree::TaxRate', adjustable_type: 'Spree::LineItem') + + attr_accessible :included_tax + + def set_included_tax!(rate) + tax = amount - (amount / (1 + rate)) + set_absolute_included_tax! tax + end + + def set_absolute_included_tax!(tax) + update_attributes! included_tax: tax.round(2) + end end end diff --git a/app/models/spree/app_configuration_decorator.rb b/app/models/spree/app_configuration_decorator.rb index f4f4acc5ec..5ad4a9a1c5 100644 --- a/app/models/spree/app_configuration_decorator.rb +++ b/app/models/spree/app_configuration_decorator.rb @@ -1,9 +1,10 @@ Spree::AppConfiguration.class_eval do - # This file decorates the existing preferences file defined by Spree. - # It allows us to add our own global configuration variables, which - # we can allow to be modified in the UI by adding appropriate form - # elements to existing or new configuration pages. + # This file decorates the existing preferences file defined by Spree. + # It allows us to add our own global configuration variables, which + # we can allow to be modified in the UI by adding appropriate form + # elements to existing or new configuration pages. - # Tax Preferences - preference :products_require_tax_category, :boolean, default: false -end \ No newline at end of file + # Tax Preferences + preference :products_require_tax_category, :boolean, default: false + preference :shipping_tax_rate, :decimal, default: 0 +end diff --git a/app/models/spree/line_item_decorator.rb b/app/models/spree/line_item_decorator.rb index 28be16d1a4..b2c0a583fc 100644 --- a/app/models/spree/line_item_decorator.rb +++ b/app/models/spree/line_item_decorator.rb @@ -26,6 +26,7 @@ Spree::LineItem.class_eval do def price_with_adjustments # EnterpriseFee#create_locked_adjustment applies adjustments on line items to their parent order, # so line_item.adjustments returns an empty array + return 0 if quantity == 0 (price + order.adjustments.where(source_id: id).sum(&:amount) / quantity).round(2) end diff --git a/app/models/spree/money_decorator.rb b/app/models/spree/money_decorator.rb new file mode 100644 index 0000000000..e179343bc5 --- /dev/null +++ b/app/models/spree/money_decorator.rb @@ -0,0 +1,7 @@ +Spree::Money.class_eval do + + # return the currency symbol (on it's own) for the current default currency + def self.currency_symbol + Money.new(0, Spree::Config[:currency]).symbol + end +end diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index c4dfeaeb64..d5e18c3d9b 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -68,7 +68,7 @@ Spree::Order.class_eval do scope :with_payment_method_name, lambda { |payment_method_name| joins(:payments => :payment_method). - where('spree_payment_methods.name = ?', payment_method_name). + where('spree_payment_methods.name IN (?)', payment_method_name). select('DISTINCT spree_orders.*') } @@ -103,13 +103,17 @@ Spree::Order.class_eval do def add_variant(variant, quantity = 1, max_quantity = nil, currency = nil) line_items(:reload) current_item = find_line_item_by_variant(variant) - if current_item - Bugsnag.notify(RuntimeError.new("Order populator weirdness"), { + + # Notify bugsnag if we get line items with a quantity of zero + if quantity == 0 + Bugsnag.notify(RuntimeError.new("Zero Quantity Line Item"), { current_item: current_item.as_json, line_items: line_items.map(&:id), - reloaded: line_items(:reload).map(&:id), variant: variant.as_json }) + end + + if current_item current_item.quantity = quantity current_item.max_quantity = max_quantity @@ -198,16 +202,21 @@ Spree::Order.class_eval do Spree::ShippingMethod.all_available(self, display_on) end + def shipping_tax + adjustments(:reload).shipping.sum &:included_tax + end + + def enterprise_fee_tax + adjustments(:reload).enterprise_fee.sum &:included_tax + end + + def total_tax + (adjustments + price_adjustments).sum &:included_tax + end + # Overrride of Spree method, that allows us to send separate confirmation emails to user and shop owners def deliver_order_confirmation_email - begin - Spree::OrderMailer.confirm_email_for_customer(self.id).deliver - Spree::OrderMailer.confirm_email_for_shop(self.id).deliver - rescue Exception => e - Bugsnag.notify(e) - logger.error("#{e.class.name}: #{e.message}") - logger.error(e.backtrace * "\n") - end + Delayed::Job.enqueue ConfirmOrderJob.new(id) end diff --git a/app/models/spree/shipment_decorator.rb b/app/models/spree/shipment_decorator.rb new file mode 100644 index 0000000000..ee9189efdd --- /dev/null +++ b/app/models/spree/shipment_decorator.rb @@ -0,0 +1,11 @@ +module Spree + Shipment.class_eval do + def ensure_correct_adjustment_with_included_tax + ensure_correct_adjustment_without_included_tax + + adjustment.set_included_tax! Config.shipping_tax_rate if Config.shipment_inc_vat + end + + alias_method_chain :ensure_correct_adjustment, :included_tax + end +end diff --git a/app/models/spree/shipping_category_decorator.rb b/app/models/spree/shipping_category_decorator.rb new file mode 100644 index 0000000000..b78ba3337c --- /dev/null +++ b/app/models/spree/shipping_category_decorator.rb @@ -0,0 +1,3 @@ +Spree::ShippingCategory.class_eval do + attr_accessible :temperature_controlled +end diff --git a/app/models/spree/tax_rate_decorator.rb b/app/models/spree/tax_rate_decorator.rb new file mode 100644 index 0000000000..835c001a05 --- /dev/null +++ b/app/models/spree/tax_rate_decorator.rb @@ -0,0 +1,12 @@ +Spree::TaxRate.class_eval do + def adjust_with_included_tax(order) + adjust_without_included_tax(order) + + order.reload + (order.adjustments.tax + order.price_adjustments).each do |a| + a.set_absolute_included_tax! a.amount + end + end + + alias_method_chain :adjust, :included_tax +end diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index d19de72d1b..b700fae3f0 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -2,7 +2,9 @@ Spree.user_class.class_eval do has_many :enterprise_roles, :dependent => :destroy has_many :enterprises, through: :enterprise_roles has_many :owned_enterprises, class_name: 'Enterprise', foreign_key: :owner_id, inverse_of: :owner + has_many :owned_groups, class_name: 'EnterpriseGroup', foreign_key: :owner_id, inverse_of: :owner has_one :cart + has_many :customers accepts_nested_attributes_for :enterprise_roles, :allow_destroy => true @@ -11,6 +13,7 @@ Spree.user_class.class_eval do validate :limit_owned_enterprises + def known_users if admin? Spree::User.scoped @@ -29,14 +32,19 @@ Spree.user_class.class_eval do end end + def customer_of(enterprise) + customers.of(enterprise).first + end + def send_signup_confirmation - Spree::UserMailer.signup_confirmation(self).deliver + Delayed::Job.enqueue ConfirmSignupJob.new(id) end def can_own_more_enterprises? owned_enterprises(:reload).size < enterprise_limit end + private def limit_owned_enterprises diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 13908382f9..86a0b5fd69 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -76,7 +76,9 @@ Spree::Variant.class_eval do def full_name return unit_to_display if display_name.blank? - display_name + " (" + unit_to_display + ")" + return display_name if display_name.downcase.include? unit_to_display.downcase + return unit_to_display if unit_to_display.downcase.include? display_name.downcase + "#{display_name} (#{unit_to_display})" end def name_to_display diff --git a/app/models/variant_override_set.rb b/app/models/variant_override_set.rb index fc02173862..985190095b 100644 --- a/app/models/variant_override_set.rb +++ b/app/models/variant_override_set.rb @@ -1,6 +1,6 @@ class VariantOverrideSet < ModelSet - def initialize(attributes={}) - super(VariantOverride, VariantOverride.all, attributes, nil, + def initialize(collection, attributes={}) + super(VariantOverride, collection, attributes, nil, proc { |attrs| attrs['price'].blank? && attrs['count_on_hand'].blank? } ) end end diff --git a/app/overrides/add_enterprise_groups_to_admin_configurations_menu.rb b/app/overrides/add_enterprise_groups_to_admin_configurations_menu.rb deleted file mode 100644 index 9fb511fc8a..0000000000 --- a/app/overrides/add_enterprise_groups_to_admin_configurations_menu.rb +++ /dev/null @@ -1,6 +0,0 @@ -Deface::Override.new(:virtual_path => "spree/admin/shared/_configuration_menu", - :name => "add_enterprise_groups_to_admin_configurations_menu", - :insert_bottom => "[data-hook='admin_configurations_sidebar_menu']", - :text => "
  • <%= link_to 'Enterprise Groups', main_app.admin_enterprise_groups_path %>
  • ", - :partial => 'enterprise_groups/admin_configurations_menu', - :original => '') diff --git a/app/overrides/add_enterprises_admin_tab.rb b/app/overrides/add_enterprises_admin_tab.rb deleted file mode 100644 index 9fbb00f5f3..0000000000 --- a/app/overrides/add_enterprises_admin_tab.rb +++ /dev/null @@ -1,5 +0,0 @@ -Deface::Override.new(:virtual_path => "spree/layouts/admin", - :name => "add_enterprises_admin_tab", - :insert_bottom => "[data-hook='admin_tabs'], #admin_tabs[data-hook]", - :text => "<%= tab :enterprises, :url => main_app.admin_enterprises_path %>", - :original => '6999548b86c700f2cc5d4f9d297c94b3617fd981') \ No newline at end of file diff --git a/app/overrides/add_order_cycles_admin_tab.rb b/app/overrides/add_order_cycles_admin_tab.rb deleted file mode 100644 index 6e36e413a8..0000000000 --- a/app/overrides/add_order_cycles_admin_tab.rb +++ /dev/null @@ -1,5 +0,0 @@ -Deface::Override.new(:virtual_path => "spree/layouts/admin", - :name => "add_order_cycles_admin_tab", - :insert_bottom => "[data-hook='admin_tabs'], #admin_tabs[data-hook]", - :text => "<%= tab :order_cycles, :url => main_app.admin_order_cycles_path %>", - :original => 'd4e321201ecb543e92192a031c8896a45dde3576') \ No newline at end of file diff --git a/app/overrides/order_item_description.rb b/app/overrides/order_item_description.rb deleted file mode 100644 index 4ec1f83a24..0000000000 --- a/app/overrides/order_item_description.rb +++ /dev/null @@ -1,5 +0,0 @@ -Deface::Override.new(:virtual_path => "spree/shared/_order_details", - :replace => "[data-hook='order_item_description']", - :partial => "spree/orders/order_item_description", - :name => "order_item_description", - :original => '1729abc5f441607b09cc0d44843a8dfd660ac5e0') \ No newline at end of file diff --git a/app/overrides/spree/admin/product_properties/_product_property_fields/replace_free_text_with_select.html.haml.deface b/app/overrides/spree/admin/product_properties/_product_property_fields/replace_free_text_with_select.html.haml.deface new file mode 100644 index 0000000000..486d35014e --- /dev/null +++ b/app/overrides/spree/admin/product_properties/_product_property_fields/replace_free_text_with_select.html.haml.deface @@ -0,0 +1,6 @@ +/ replace_contents "td.property_name" + +- if spree_current_user.admin? + = f.text_field :property_name, :class => 'autocomplete' +- else + = f.select :property_name, @properties, { :include_blank => true }, { class: 'select2 fullwidth' } diff --git a/app/overrides/spree/admin/shipping_categories/_form/add_temperature_controlled_form_element.html.haml.deface b/app/overrides/spree/admin/shipping_categories/_form/add_temperature_controlled_form_element.html.haml.deface new file mode 100644 index 0000000000..d447471961 --- /dev/null +++ b/app/overrides/spree/admin/shipping_categories/_form/add_temperature_controlled_form_element.html.haml.deface @@ -0,0 +1,5 @@ +/ insert_bottom "div[data-hook='admin_shipping_category_form_fields']" + +%div.field.align-center{"data-hook" => "name"} + = f.label :temperature_controlled, t(:temperature_controlled) + = f.check_box :temperature_controlled diff --git a/app/overrides/spree/admin/shipping_categories/index/add_temp_controlled_td.html.haml.deface b/app/overrides/spree/admin/shipping_categories/index/add_temp_controlled_td.html.haml.deface new file mode 100644 index 0000000000..aaafec5607 --- /dev/null +++ b/app/overrides/spree/admin/shipping_categories/index/add_temp_controlled_td.html.haml.deface @@ -0,0 +1,5 @@ +/ insert_after "[data-hook='category_row'] td:first-child" + +%td.align-center + = shipping_category.temperature_controlled ? 'Yes' : 'No' + %br/ diff --git a/app/overrides/spree/admin/shipping_categories/index/add_temp_controlled_th.html.haml.deface b/app/overrides/spree/admin/shipping_categories/index/add_temp_controlled_th.html.haml.deface new file mode 100644 index 0000000000..561e59bf91 --- /dev/null +++ b/app/overrides/spree/admin/shipping_categories/index/add_temp_controlled_th.html.haml.deface @@ -0,0 +1,4 @@ +/ insert_after "[data-hook='categories_header'] th:first-child" + +%th + Temperature Controlled diff --git a/app/overrides/spree/admin/tax_settings/edit/shipping_tax_rate.html.haml.deface b/app/overrides/spree/admin/tax_settings/edit/shipping_tax_rate.html.haml.deface new file mode 100644 index 0000000000..b378ba84a6 --- /dev/null +++ b/app/overrides/spree/admin/tax_settings/edit/shipping_tax_rate.html.haml.deface @@ -0,0 +1,5 @@ +/ insert_after "[data-hook='shipment_vat']" + +.field.align-center{ "data-hook" => "shipping_tax_rate" } + = number_field_tag "preferences[shipping_tax_rate]", Spree::Config[:shipping_tax_rate].to_f, in: 0.0..1.0, step: 0.01 + = label_tag nil, t(:shipping_tax_rate) \ No newline at end of file diff --git a/app/overrides/spree/layouts/admin/add_enterprises_admin_tab.html.haml.deface b/app/overrides/spree/layouts/admin/add_enterprises_admin_tab.html.haml.deface new file mode 100644 index 0000000000..cddadabed7 --- /dev/null +++ b/app/overrides/spree/layouts/admin/add_enterprises_admin_tab.html.haml.deface @@ -0,0 +1,2 @@ +/ insert_bottom "[data-hook='admin_tabs'], #admin_tabs[data-hook]" += tab :enterprises, :url => main_app.admin_enterprises_path diff --git a/app/overrides/spree/layouts/admin/add_groups_admin_tab.html.haml.deface b/app/overrides/spree/layouts/admin/add_groups_admin_tab.html.haml.deface new file mode 100644 index 0000000000..fe303a453c --- /dev/null +++ b/app/overrides/spree/layouts/admin/add_groups_admin_tab.html.haml.deface @@ -0,0 +1,2 @@ +/ insert_bottom "[data-hook='admin_tabs'], #admin_tabs[data-hook]" += tab :enterprise_groups, :url => main_app.admin_enterprise_groups_path, label: 'groups' diff --git a/app/overrides/spree/layouts/admin/add_order_cycles_admin_tab.html.haml.deface b/app/overrides/spree/layouts/admin/add_order_cycles_admin_tab.html.haml.deface new file mode 100644 index 0000000000..90b9007de5 --- /dev/null +++ b/app/overrides/spree/layouts/admin/add_order_cycles_admin_tab.html.haml.deface @@ -0,0 +1,2 @@ +/ insert_bottom "[data-hook='admin_tabs'], #admin_tabs[data-hook]" += tab :order_cycles, :url => main_app.admin_order_cycles_path diff --git a/app/presenters/enterprise_fee_presenter.rb b/app/presenters/enterprise_fee_presenter.rb index a0a6d8460a..b8f9ae4655 100644 --- a/app/presenters/enterprise_fee_presenter.rb +++ b/app/presenters/enterprise_fee_presenter.rb @@ -3,7 +3,7 @@ class EnterpriseFeePresenter @controller, @enterprise_fee, @index = controller, enterprise_fee, index end - delegate :id, :enterprise_id, :fee_type, :name, :calculator_type, :to => :enterprise_fee + delegate :id, :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type, :to => :enterprise_fee def enterprise_fee @enterprise_fee diff --git a/app/serializers/api/admin/basic_enterprise_fee_serializer.rb b/app/serializers/api/admin/basic_enterprise_fee_serializer.rb new file mode 100644 index 0000000000..07bcaffc0c --- /dev/null +++ b/app/serializers/api/admin/basic_enterprise_fee_serializer.rb @@ -0,0 +1,3 @@ +class Api::Admin::BasicEnterpriseFeeSerializer < ActiveModel::Serializer + attributes :id, :enterprise_id +end diff --git a/app/serializers/api/admin/enterprise_fee_serializer.rb b/app/serializers/api/admin/enterprise_fee_serializer.rb new file mode 100644 index 0000000000..1b5a201b65 --- /dev/null +++ b/app/serializers/api/admin/enterprise_fee_serializer.rb @@ -0,0 +1,23 @@ +class Api::Admin::EnterpriseFeeSerializer < ActiveModel::Serializer + attributes :id, :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type + attributes :enterprise_name, :calculator_description, :calculator_settings + + def enterprise_name + object.enterprise.andand.name + end + + def calculator_description + object.calculator.andand.description + end + + def calculator_settings + result = nil + + options[:controller].send(:with_format, :html) do + result = options[:controller].render_to_string :partial => 'admin/enterprise_fees/calculator_settings', :locals => {:enterprise_fee => object} + end + + result.gsub('[0]', '[{{ $index }}]').gsub('_0_', '_{{ $index }}_') + end + +end diff --git a/app/serializers/api/admin/exchange_serializer.rb b/app/serializers/api/admin/exchange_serializer.rb new file mode 100644 index 0000000000..64c2c08cd4 --- /dev/null +++ b/app/serializers/api/admin/exchange_serializer.rb @@ -0,0 +1,17 @@ +class Api::Admin::ExchangeSerializer < ActiveModel::Serializer + attributes :id, :sender_id, :receiver_id, :incoming, :variants, :pickup_time, :pickup_instructions + + has_many :enterprise_fees, serializer: Api::Admin::BasicEnterpriseFeeSerializer + + def variants + permitted = Spree::Variant.where("1=0") + if object.incoming + permitted = OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object.order_cycle). + visible_variants_for_incoming_exchanges_from(object.sender) + else + permitted = OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object.order_cycle). + visible_variants_for_outgoing_exchanges_to(object.receiver) + end + Hash[ object.variants.merge(permitted).map { |v| [v.id, true] } ] + end +end diff --git a/app/serializers/api/admin/for_order_cycle/enterprise_serializer.rb b/app/serializers/api/admin/for_order_cycle/enterprise_serializer.rb new file mode 100644 index 0000000000..46afca46c5 --- /dev/null +++ b/app/serializers/api/admin/for_order_cycle/enterprise_serializer.rb @@ -0,0 +1,13 @@ +class Api::Admin::ForOrderCycle::EnterpriseSerializer < ActiveModel::Serializer + attributes :id, :name, :managed, :supplied_products + + def managed + Enterprise.managed_by(options[:spree_current_user]).include? object + end + + def supplied_products + objects = object.supplied_products.not_deleted + serializer = Api::Admin::ForOrderCycle::SuppliedProductSerializer + ActiveModel::ArraySerializer.new(objects, each_serializer: serializer ) + end +end diff --git a/app/serializers/api/admin/for_order_cycle/supplied_product_serializer.rb b/app/serializers/api/admin/for_order_cycle/supplied_product_serializer.rb new file mode 100644 index 0000000000..dbe3ec7a48 --- /dev/null +++ b/app/serializers/api/admin/for_order_cycle/supplied_product_serializer.rb @@ -0,0 +1,21 @@ +class Api::Admin::ForOrderCycle::SuppliedProductSerializer < ActiveModel::Serializer + attributes :name, :supplier_name, :image_url, :master_id, :variants + + def supplier_name + object.supplier.andand.name + end + + def image_url + object.images.present? ? object.images.first.attachment.url(:mini) : nil + end + + def master_id + object.master.id + end + + def variants + object.variants.map do |variant| + { id: variant.id, label: variant.full_name } + end + end +end diff --git a/app/serializers/api/admin/order_cycle_serializer.rb b/app/serializers/api/admin/order_cycle_serializer.rb new file mode 100644 index 0000000000..d305612790 --- /dev/null +++ b/app/serializers/api/admin/order_cycle_serializer.rb @@ -0,0 +1,64 @@ +class Api::Admin::OrderCycleSerializer < ActiveModel::Serializer + attributes :id, :name, :orders_open_at, :orders_close_at, :coordinator_id, :exchanges + attributes :editable_variants_for_incoming_exchanges, :editable_variants_for_outgoing_exchanges + attributes :visible_variants_for_outgoing_exchanges + attributes :viewing_as_coordinator + + has_many :coordinator_fees, serializer: Api::IdSerializer + + def orders_open_at + object.orders_open_at.to_s + end + + def orders_close_at + object.orders_close_at.to_s + end + + def viewing_as_coordinator + Enterprise.managed_by(options[:current_user]).include? object.coordinator + end + + def exchanges + scoped_exchanges = OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object).visible_exchanges.order('id ASC') + ActiveModel::ArraySerializer.new(scoped_exchanges, {each_serializer: Api::Admin::ExchangeSerializer, current_user: options[:current_user] }) + end + + def editable_variants_for_incoming_exchanges + # For each enterprise that the current user is able to see in this order cycle, + # work out which variants should be editable within incoming exchanges from that enterprise + editable = {} + permissions = OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object) + enterprises = permissions.visible_enterprises + enterprises.each do |enterprise| + variants = permissions.editable_variants_for_incoming_exchanges_from(enterprise).pluck(:id) + editable[enterprise.id] = variants if variants.any? + end + editable + end + + def editable_variants_for_outgoing_exchanges + # For each enterprise that the current user is able to see in this order cycle, + # work out which variants should be editable within incoming exchanges from that enterprise + editable = {} + permissions = OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object) + enterprises = permissions.visible_enterprises + enterprises.each do |enterprise| + variants = permissions.editable_variants_for_outgoing_exchanges_to(enterprise).pluck(:id) + editable[enterprise.id] = variants if variants.any? + end + editable + end + + def visible_variants_for_outgoing_exchanges + # For each enterprise that the current user is able to see in this order cycle, + # work out which variants should be visible within outgoing exchanges from that enterprise + visible = {} + permissions = OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object) + enterprises = permissions.visible_enterprises + enterprises.each do |enterprise| + variants = permissions.visible_variants_for_outgoing_exchanges_to(enterprise).pluck(:id) + visible[enterprise.id] = variants if variants.any? + end + visible + end +end diff --git a/app/serializers/api/enterprise_serializer.rb b/app/serializers/api/enterprise_serializer.rb index 95ef4cec60..532887ae01 100644 --- a/app/serializers/api/enterprise_serializer.rb +++ b/app/serializers/api/enterprise_serializer.rb @@ -36,12 +36,10 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer :long_description, :website, :instagram, :linkedin, :twitter, :facebook, :is_primary_producer, :is_distributor, :phone, :visible, :email, :hash, :logo, :promo_image, :path, :pickup, :delivery, - :icon, :icon_font, :producer_icon_font, :category + :icon, :icon_font, :producer_icon_font, :category, :producers, :hubs has_many :distributed_taxons, key: :taxons, serializer: Api::IdSerializer has_many :supplied_taxons, serializer: Api::IdSerializer - has_many :distributors, key: :hubs, serializer: Api::IdSerializer - has_many :suppliers, key: :producers, serializer: Api::IdSerializer has_one :address, serializer: Api::AddressSerializer @@ -73,6 +71,14 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer enterprise_shop_path(object) end + def producers + ActiveModel::ArraySerializer.new(object.suppliers.activated, {each_serializer: Api::IdSerializer}) + end + + def hubs + ActiveModel::ArraySerializer.new(object.distributors.activated, {each_serializer: Api::IdSerializer}) + end + # Map svg icons. def icon icons = { diff --git a/app/serializers/api/variant_serializer.rb b/app/serializers/api/variant_serializer.rb index 5871d6def4..f995fa4345 100644 --- a/app/serializers/api/variant_serializer.rb +++ b/app/serializers/api/variant_serializer.rb @@ -1,6 +1,6 @@ class Api::VariantSerializer < ActiveModel::Serializer attributes :id, :is_master, :count_on_hand, :name_to_display, :unit_to_display, - :on_demand, :price, :fees, :price_with_fees + :options_text, :on_demand, :price, :fees, :price_with_fees, :product_name def price_with_fees object.price_with_fees(options[:current_distributor], options[:current_order_cycle]) @@ -13,4 +13,9 @@ class Api::VariantSerializer < ActiveModel::Serializer def fees object.fees_by_type_for(options[:current_distributor], options[:current_order_cycle]) end + + def product_name + object.product.name + end + end diff --git a/app/views/admin/enterprise_fees/index.html.haml b/app/views/admin/enterprise_fees/index.html.haml index ba3c3e6f4c..08199d4c4a 100644 --- a/app/views/admin/enterprise_fees/index.html.haml +++ b/app/views/admin/enterprise_fees/index.html.haml @@ -13,6 +13,7 @@ %th Enterprise %th Fee Type %th Name + %th Tax Category %th Calculator %th Calculator values %th.actions @@ -21,9 +22,10 @@ %tr{'ng-repeat' => 'enterprise_fee in enterprise_fees | filter:query'} %td = f.ng_hidden_field :id - = f.ng_collection_select :enterprise_id, @enterprises, :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, { placeholder: 'e.g. packing fee' } + %td= f.ng_collection_select :tax_category_id, @tax_categories, :id, :name, 'enterprise_fee.tax_category_id', include_blank: true %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"} %td{'ng-bind-html-unsafe-compiled' => 'enterprise_fee.calculator_settings'} %td.actions{'spree-delete-resource' => "1"} diff --git a/app/views/admin/enterprise_fees/index.rep b/app/views/admin/enterprise_fees/index.rep index 61192ca786..8dc24b5d74 100644 --- a/app/views/admin/enterprise_fees/index.rep +++ b/app/views/admin/enterprise_fees/index.rep @@ -4,6 +4,7 @@ r.list_of :enterprise_fees, @presented_collection do r.element :enterprise_name r.element :fee_type r.element :name + r.element :tax_category_id r.element :calculator_type r.element :calculator_description r.element :calculator_settings if @include_calculators diff --git a/app/views/admin/enterprise_groups/_form.html.haml b/app/views/admin/enterprise_groups/_form.html.haml index ec4934bf19..39a4b8bd1e 100644 --- a/app/views/admin/enterprise_groups/_form.html.haml +++ b/app/views/admin/enterprise_groups/_form.html.haml @@ -1,43 +1,16 @@ -= f.field_container :name do - = f.label :name - %br/ - = f.text_field :name += render 'spree/shared/error_messages', target: @enterprise -= f.field_container :description do - = f.label :description - %br/ - = f.text_field :description - -= f.field_container :long_description do - = f.label :long_description - %br/ - = f.text_area :long_description - -= f.field_container :on_front_page do - = f.label :on_front_page, 'On front page?' - %br/ - = f.check_box :on_front_page - -= f.field_container :enterprise_ids do - = f.label :enterprise_ids, 'Enterprises' - %br/ - = f.collection_select :enterprise_ids, Enterprise.all, :id, :name, {}, {class: "select2 fullwidth", multiple: true} - - -.row - .alpha.three.columns - = f.label :logo, class: 'with-tip', 'data-powertip' => 'This is the logo' - .with-tip{'data-powertip' => 'This is the logo'} - %a What's this? - .omega.eight.columns - = image_tag @object.logo.url if @object.logo.present? - = f.file_field :logo - -.row - .alpha.three.columns - = f.label :promo_image, class: 'with-tip', 'data-powertip' => 'This image is displayed at the top of the Group profile' - .with-tip{'data-powertip' => 'This image is displayed at the top of the Group profile'} - %a What's this? - .omega.eight.columns - = image_tag @object.promo_image.url if @object.promo_image.present? - = f.file_field :promo_image += form_for [main_app, :admin, @enterprise_group] do |f| + .row{ ng: {app: 'admin.enterprise_groups', controller: 'enterpriseGroupCtrl'} } + .sixteen.columns.alpha + .four.columns.alpha + = render 'admin/shared/side_menu' + .one.column   + .eleven.columns.omega.fullwidth_inputs + = render 'form_primary_details', f: f + = render 'form_users', f: f + = render 'form_about', f: f + = render 'form_images', f: f + = render 'form_address', f: f + = render 'form_web', f: f + = render "spree/admin/shared/#{action}_resource_links" diff --git a/app/views/admin/enterprise_groups/_form_about.html.haml b/app/views/admin/enterprise_groups/_form_about.html.haml new file mode 100644 index 0000000000..a29fde22cd --- /dev/null +++ b/app/views/admin/enterprise_groups/_form_about.html.haml @@ -0,0 +1,6 @@ +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='About'" } } + %legend About + = f.field_container :long_description do + %text-angular{'id' => 'enterprise_group_long_description', 'name' => 'enterprise_group[long_description]', 'class' => 'text-angular', + 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]"} + != @enterprise_group[:long_description] diff --git a/app/views/admin/enterprise_groups/_form_address.html.haml b/app/views/admin/enterprise_groups/_form_address.html.haml new file mode 100644 index 0000000000..12bb7ef315 --- /dev/null +++ b/app/views/admin/enterprise_groups/_form_address.html.haml @@ -0,0 +1,41 @@ += f.fields_for :address do |af| + %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Contact'" } } + %legend Contact + .row + .alpha.three.columns + = af.label :phone + .omega.eight.columns + = af.text_field :phone, { placeholder: "eg. 98 7654 3210"} + .row + .alpha.three.columns + = f.label :email + .omega.eight.columns + = f.text_field :email + .row + .three.columns.alpha + = af.label :address1 + .eight.columns.omega + = af.text_field :address1, { placeholder: "eg. 123 High Street"} + .row + .alpha.three.columns + = af.label :address2 + .eight.columns.omega + = af.text_field :address2 + .row + .three.columns.alpha + = af.label :city, 'Suburb' + \/ + = af.label :zipcode, 'Postcode' + .four.columns + = af.text_field :city, { placeholder: "eg. Northcote"} + .four.columns.omega + = af.text_field :zipcode, { placeholder: "eg. 3070"} + .row + .three.columns.alpha + = af.label :state_id, 'State' + \/ + = af.label :country_id, 'Country' + .four.columns + = af.collection_select :state_id, af.object.country.states, :id, :name, {}, :class => "select2 fullwidth" + .four.columns.omega + = af.collection_select :country_id, available_countries, :id, :name, {}, :class => "select2 fullwidth" diff --git a/app/views/admin/enterprise_groups/_form_images.html.haml b/app/views/admin/enterprise_groups/_form_images.html.haml new file mode 100644 index 0000000000..49169851c3 --- /dev/null +++ b/app/views/admin/enterprise_groups/_form_images.html.haml @@ -0,0 +1,18 @@ +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Images'" } } + %legend Images + .row + .alpha.three.columns + = f.label :logo, class: 'with-tip', 'data-powertip' => 'This is the logo' + .with-tip{'data-powertip' => 'This is the logo'} + %a What's this? + .omega.eight.columns + = image_tag @object.logo.url if @object.logo.present? + = f.file_field :logo + .row + .alpha.three.columns + = f.label :promo_image, class: 'with-tip', 'data-powertip' => 'This image is displayed at the top of the Group profile' + .with-tip{'data-powertip' => 'This image is displayed at the top of the Group profile'} + %a What's this? + .omega.eight.columns + = image_tag @object.promo_image.url if @object.promo_image.present? + = f.file_field :promo_image diff --git a/app/views/admin/enterprise_groups/_form_primary_details.html.haml b/app/views/admin/enterprise_groups/_form_primary_details.html.haml new file mode 100644 index 0000000000..6d326e33fa --- /dev/null +++ b/app/views/admin/enterprise_groups/_form_primary_details.html.haml @@ -0,0 +1,21 @@ +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Primary Details'" } } + %legend Primary Details + = f.field_container :name do + = f.label :name + %br/ + = f.text_field :name + + = f.field_container :description do + = f.label :description + %br/ + = f.text_field :description + + = f.field_container :on_front_page do + = f.label :on_front_page, 'On front page?' + %br/ + = f.check_box :on_front_page + + = f.field_container :enterprise_ids do + = f.label :enterprise_ids, 'Enterprises' + %br/ + = f.collection_select :enterprise_ids, @enterprises, :id, :name, {}, {class: "select2 fullwidth", multiple: true} diff --git a/app/views/admin/enterprise_groups/_form_users.html.haml b/app/views/admin/enterprise_groups/_form_users.html.haml new file mode 100644 index 0000000000..0a8a5dd635 --- /dev/null +++ b/app/views/admin/enterprise_groups/_form_users.html.haml @@ -0,0 +1,14 @@ +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Users'" } } + %legend Users + .row + .three.columns.alpha + =f.label :owner_id, 'Owner' + .with-tip{'data-powertip' => "The primary user responsible for this group."} + %a What's this? + .eight.columns.omega + - if spree_current_user.admin? + = f.hidden_field :owner_id, + class: "select2 fullwidth", + 'user-select' => "{id:'#{@enterprise_group.owner.andand.id}', email:'#{@enterprise_group.owner.andand.email}'}" + - else + = @enterprise_group.owner.andand.email diff --git a/app/views/admin/enterprise_groups/_form_web.html.haml b/app/views/admin/enterprise_groups/_form_web.html.haml new file mode 100644 index 0000000000..42638d94c6 --- /dev/null +++ b/app/views/admin/enterprise_groups/_form_web.html.haml @@ -0,0 +1,27 @@ +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Web'" } } + %legend Web Resources + .row + .alpha.three.columns + = f.label :website + .omega.eight.columns + = f.text_field :website, { placeholder: "eg. www.truffles.com"} + .row + .alpha.three.columns + = f.label :facebook, 'Facebook' + .omega.eight.columns + = f.text_field :facebook + .row + .alpha.three.columns + = f.label :instagram, 'Instagram' + .omega.eight.columns + = f.text_field :instagram + .row + .alpha.three.columns + = f.label :linkedin, 'LinkedIn' + .omega.eight.columns + = f.text_field :linkedin + .row + .alpha.three.columns + = f.label :twitter + .omega.eight.columns + = f.text_field :twitter, { placeholder: "eg. @the_prof" } diff --git a/app/views/admin/enterprise_groups/edit.html.haml b/app/views/admin/enterprise_groups/edit.html.haml index 19c954f8e4..d83367b8c5 100644 --- a/app/views/admin/enterprise_groups/edit.html.haml +++ b/app/views/admin/enterprise_groups/edit.html.haml @@ -1,5 +1,3 @@ = render :partial => 'spree/shared/error_messages', :locals => { :target => @enterprise } -= form_for [main_app, :admin, @enterprise_group] do |f| - = render :partial => 'form', :locals => { :f => f } - = render :partial => 'spree/admin/shared/edit_resource_links' += render 'admin/enterprise_groups/form', action: 'edit' diff --git a/app/views/admin/enterprise_groups/index.html.haml b/app/views/admin/enterprise_groups/index.html.haml index 035402267f..13bc19e364 100644 --- a/app/views/admin/enterprise_groups/index.html.haml +++ b/app/views/admin/enterprise_groups/index.html.haml @@ -9,6 +9,8 @@ %thead %tr %th Name + - if spree_current_user.admin? + %th Owner %th On front page? %th Enterprises %th.actions @@ -17,14 +19,18 @@ - @enterprise_groups.each do |enterprise_group| %tr %td.name= enterprise_group.name + - if spree_current_user.admin? + %td= enterprise_group.owner.andand.email || "" %td= enterprise_group.on_front_page ? 'Y' : 'N' %td= enterprise_group.enterprises.map(&:name).join ', ' %td.actions = link_to '', main_app.edit_admin_enterprise_group_path(enterprise_group), class: 'edit-enterprise-group icon-edit no-text' = link_to_delete enterprise_group, no_text: true - - if enterprise_group.last? - .blank-action - - else - = link_to_with_icon 'icon-arrow-down', '', main_app.admin_enterprise_group_move_down_path(enterprise_group), class: 'move-down no-text' - = link_to_with_icon 'icon-arrow-up', '', main_app.admin_enterprise_group_move_up_path(enterprise_group), class: 'move-up no-text' unless enterprise_group.first? + - if spree_current_user.admin? + - if enterprise_group.last? + .blank-action + - else + = link_to_with_icon 'icon-arrow-down', '', main_app.admin_enterprise_group_move_down_path(enterprise_group), class: 'move-down no-text' + - if !enterprise_group.first? + = link_to_with_icon 'icon-arrow-up', '', main_app.admin_enterprise_group_move_up_path(enterprise_group), class: 'move-up no-text' diff --git a/app/views/admin/enterprise_groups/new.html.haml b/app/views/admin/enterprise_groups/new.html.haml index f899eaa380..73d597a920 100644 --- a/app/views/admin/enterprise_groups/new.html.haml +++ b/app/views/admin/enterprise_groups/new.html.haml @@ -1,5 +1,3 @@ = render :partial => 'spree/shared/error_messages', :locals => { :target => @enterprise } -= form_for [main_app, :admin, @enterprise_group] do |f| - = render :partial => 'form', :locals => { :f => f } - = render :partial => 'spree/admin/shared/new_resource_links' += render 'admin/enterprise_groups/form', action: 'new' diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index aa2ae7f589..28f76d34d0 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -31,6 +31,10 @@ %legend Images = render 'admin/enterprises/form/images', f: f +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Properties'" } } + %legend Properties + = render 'admin/enterprises/form/properties', f: f + %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Shipping Methods'" } } %legend Shipping Methods = render 'admin/enterprises/form/shipping_methods', f: f diff --git a/app/views/admin/enterprises/_ng_form.html.haml b/app/views/admin/enterprises/_ng_form.html.haml index 91a3c4e3aa..a19bc43314 100644 --- a/app/views/admin/enterprises/_ng_form.html.haml +++ b/app/views/admin/enterprises/_ng_form.html.haml @@ -10,7 +10,7 @@ .row .sixteen.columns.alpha .four.columns.alpha - = render 'side_menu' + = render 'admin/shared/side_menu' .one.column   .eleven.columns.omega.fullwidth_inputs = render 'form', f: f diff --git a/app/views/admin/enterprises/_supplied_product.rabl b/app/views/admin/enterprises/_supplied_product.rabl deleted file mode 100644 index 268ff302d5..0000000000 --- a/app/views/admin/enterprises/_supplied_product.rabl +++ /dev/null @@ -1,10 +0,0 @@ -object @product - -attributes :name -node(:supplier_name) { |p| p.supplier.andand.name } -node(:image_url) { |p| p.images.present? ? p.images.first.attachment.url(:mini) : nil } -node(:master_id) { |p| p.master.id } -child variants: :variants do |variant| - attributes :id - node(:label) { |v| v.options_text } -end diff --git a/app/views/admin/enterprises/for_order_cycle.rabl b/app/views/admin/enterprises/for_order_cycle.rabl deleted file mode 100644 index 1342d6eb1a..0000000000 --- a/app/views/admin/enterprises/for_order_cycle.rabl +++ /dev/null @@ -1,9 +0,0 @@ -collection @collection - -attributes :id, :name - -node(:supplied_products) do |enterprise| - enterprise.supplied_products.not_deleted.map do |product| - partial 'admin/enterprises/supplied_product', object: product - end -end diff --git a/app/views/admin/enterprises/form/_properties.html.haml b/app/views/admin/enterprises/form/_properties.html.haml new file mode 100644 index 0000000000..795a104f1a --- /dev/null +++ b/app/views/admin/enterprises/form/_properties.html.haml @@ -0,0 +1,12 @@ += render 'admin/producer_properties/form', f: f + +// :javascript +// var properties = #{raw(@properties.to_json)}; +// +// $("#producer_properties input.autocomplete").live("keydown", function() { +// already_auto_completed = $(this).is('ac_input'); +// if (!already_auto_completed) { +// $(this).autocomplete({source: properties}); +// $(this).focus(); +// } +// }); diff --git a/app/views/admin/enterprises/index.html.haml b/app/views/admin/enterprises/index.html.haml index 64b8423baa..8269cd3699 100644 --- a/app/views/admin/enterprises/index.html.haml +++ b/app/views/admin/enterprises/index.html.haml @@ -10,6 +10,10 @@ = render :partial => 'spree/shared/error_messages', :locals => { :target => @enterprise_set } +-# For purposes of debugging bulk_update. See Admin/Enterprises#bulk_update. +- if flash[:action] + %p= flash[:action] + = form_for @enterprise_set, url: main_app.bulk_update_admin_enterprises_path do |f| %table#listing_enterprises.index %colgroup diff --git a/app/views/admin/order_cycles/_exchange_distributed_products_form.html.haml b/app/views/admin/order_cycles/_exchange_distributed_products_form.html.haml index 6566ee770a..1616561a8d 100644 --- a/app/views/admin/order_cycles/_exchange_distributed_products_form.html.haml +++ b/app/views/admin/order_cycles/_exchange_distributed_products_form.html.haml @@ -1,17 +1,22 @@ %td{:colspan => 3} .exchange-select-all-variants %label - = check_box_tag 'order_cycle_outgoing_exchange_{{ $parent.$index }}_select_all_variants', 1, 1, 'ng-model' => 'exchange.select_all_variants', 'ng-change' => 'setExchangeVariants(exchange, incomingExchangesVariants(), exchange.select_all_variants)', 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$index }}_select_all_variants' + = check_box_tag 'order_cycle_outgoing_exchange_{{ $parent.$index }}_select_all_variants', 1, 1, 'ng-model' => 'exchange.select_all_variants', 'ng-change' => 'setExchangeVariants(exchange, incomingExchangeVariantsFor(exchange.enterprise_id), exchange.select_all_variants)', 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$index }}_select_all_variants' Select all - .exchange-product{'ng-repeat' => 'product in supplied_products | filter:productSuppliedToOrderCycle'} + -# Scope product list based on permissions the current user has to view variants in this exchange + .exchange-product{'ng-repeat' => 'product in supplied_products | filter:productSuppliedToOrderCycle | visibleProducts:exchange:order_cycle.visible_variants_for_outgoing_exchanges' } .exchange-product-details .supplier {{ product.supplier_name }} %label - = check_box_tag 'order_cycle_outgoing_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', 1, 1, 'ng-hide' => 'product.variants.length > 0', 'ng-disabled' => 'product.variants.length > 0', 'ng-model' => 'exchange.variants[product.master_id]', 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}' + = check_box_tag 'order_cycle_outgoing_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', 1, 1, 'ng-hide' => 'product.variants.length > 0', 'ng-model' => 'exchange.variants[product.master_id]', 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', + 'ng-disabled' => 'product.variants.length > 0 || !order_cycle.editable_variants_for_outgoing_exchanges.hasOwnProperty(exchange.enterprise_id) || order_cycle.editable_variants_for_outgoing_exchanges[exchange.enterprise_id].indexOf(product.master_id) < 0' %img{'ng-src' => '{{ product.image_url }}'} {{ product.name }} + + -# if we ever need to filter variants within a product using visibility permissions, we can use this filter: visibleVariants:exchange:order_cycle.visible_variants_for_outgoing_exchanges .exchange-product-variant{'ng-repeat' => 'variant in product.variants | filter:variantSuppliedToOrderCycle'} %label - = check_box_tag 'order_cycle_outgoing_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}', 1, 1, 'ng-model' => 'exchange.variants[variant.id]', 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}' + = check_box_tag 'order_cycle_outgoing_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}', 1, 1, 'ng-model' => 'exchange.variants[variant.id]', 'id' => 'order_cycle_outgoing_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}', + 'ng-disabled' => '!order_cycle.editable_variants_for_outgoing_exchanges.hasOwnProperty(exchange.enterprise_id) || order_cycle.editable_variants_for_outgoing_exchanges[exchange.enterprise_id].indexOf(variant.id) < 0' {{ variant.label }} diff --git a/app/views/admin/order_cycles/_exchange_form.html.haml b/app/views/admin/order_cycles/_exchange_form.html.haml index 8f81b9f03f..7f0fb983f9 100644 --- a/app/views/admin/order_cycles/_exchange_form.html.haml +++ b/app/views/admin/order_cycles/_exchange_form.html.haml @@ -5,22 +5,22 @@ - if type == 'supplier' {{ enterpriseTotalVariants(enterprises[exchange.enterprise_id]) }} - else - {{ incomingExchangesVariants().length }} + {{ (incomingExchangeVariantsFor(exchange.enterprise_id)).length }} selected - if type == 'distributor' %td.collection-details - = text_field_tag 'order_cycle_outgoing_exchange_{{ $index }}_pickup_time', '', 'id' => 'order_cycle_outgoing_exchange_{{ $index }}_pickup_time', 'placeholder' => 'Ready for (ie. Date / Time)', 'ng-model' => 'exchange.pickup_time' + = text_field_tag 'order_cycle_outgoing_exchange_{{ $index }}_pickup_time', '', 'id' => 'order_cycle_outgoing_exchange_{{ $index }}_pickup_time', 'placeholder' => 'Ready for (ie. Date / Time)', 'ng-model' => 'exchange.pickup_time', 'ng-disabled' => '!enterprises[exchange.enterprise_id].managed && !order_cycle.viewing_as_coordinator' %br/ - = text_field_tag 'order_cycle_outgoing_exchange_{{ $index }}_pickup_instructions', '', 'id' => 'order_cycle_outgoing_exchange_{{ $index }}_pickup_instructions', 'placeholder' => 'Pick-up instructions', 'ng-model' => 'exchange.pickup_instructions' + = text_field_tag 'order_cycle_outgoing_exchange_{{ $index }}_pickup_instructions', '', 'id' => 'order_cycle_outgoing_exchange_{{ $index }}_pickup_instructions', 'placeholder' => 'Pick-up instructions', 'ng-model' => 'exchange.pickup_instructions', 'ng-disabled' => '!enterprises[exchange.enterprise_id].managed && !order_cycle.viewing_as_coordinator' %td.fees - %ol + %ol{ ng: { show: 'enterprises[exchange.enterprise_id].managed || order_cycle.viewing_as_coordinator' } } %li{'ng-repeat' => 'enterprise_fee in exchange.enterprise_fees'} - = select_tag 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_enterprise_id', nil, {'id' => 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_enterprise_id', 'ng-model' => 'enterprise_fee.enterprise_id', 'ng-options' => 'enterprise.id as enterprise.name for enterprise in participatingEnterprises()'} + = select_tag 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_enterprise_id', nil, {'id' => 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_enterprise_id', 'ng-model' => 'enterprise_fee.enterprise_id', 'ng-options' => 'enterprise.id as enterprise.name for enterprise in enterprisesWithFees()'} = select_tag 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_enterprise_fee_id', nil, {'id' => 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_enterprise_fee_id', 'ng-model' => 'enterprise_fee.id', 'ng-options' => 'enterprise_fee.id as enterprise_fee.name for enterprise_fee in enterpriseFeesForEnterprise(enterprise_fee.enterprise_id)'} = link_to 'Remove', '#', {'id' => 'order_cycle_{{ exchangeDirection(exchange) }}_exchange_{{ $parent.$index }}_enterprise_fees_{{ $index }}_remove', 'ng-click' => 'removeExchangeFee($event, exchange, $index)'} - = f.submit 'Add fee', 'ng-click' => 'addExchangeFee($event, exchange)' + = f.submit 'Add fee', 'ng-click' => 'addExchangeFee($event, exchange)', 'ng-hide' => '!enterprises[exchange.enterprise_id].managed && !order_cycle.viewing_as_coordinator' %td.actions %a{'ng-click' => 'removeExchange($event, exchange)', :class => "icon-trash no-text remove-exchange"} diff --git a/app/views/admin/order_cycles/_exchange_supplied_products_form.html.haml b/app/views/admin/order_cycles/_exchange_supplied_products_form.html.haml index 4e4ae4c3f3..869a862a1b 100644 --- a/app/views/admin/order_cycles/_exchange_supplied_products_form.html.haml +++ b/app/views/admin/order_cycles/_exchange_supplied_products_form.html.haml @@ -5,11 +5,14 @@ = check_box_tag 'order_cycle_incoming_exchange_{{ $parent.$index }}_select_all_variants', 1, 1, 'ng-model' => 'exchange.select_all_variants', 'ng-change' => 'setExchangeVariants(exchange, suppliedVariants(exchange.enterprise_id), exchange.select_all_variants)', 'id' => 'order_cycle_incoming_exchange_{{ $parent.$index }}_select_all_variants' Select all + -# No need to scope product list based on permissions, because if an incoming exchange is visible, + -# then all of the variants within it should be visible. May change in the future? .exchange-product{'ng-repeat' => 'product in enterprises[exchange.enterprise_id].supplied_products'} .exchange-product-details %label - = check_box_tag 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', 1, 1, 'ng-hide' => 'product.variants.length > 0', 'ng-disabled' => 'product.variants.length > 0', 'ng-model' => 'exchange.variants[product.master_id]', 'ofn-sync-distributions' => '{{ product.master_id }}', 'id' => 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}' + = check_box_tag 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', 1, 1, 'ng-hide' => 'product.variants.length > 0', 'ng-model' => 'exchange.variants[product.master_id]', 'ofn-sync-distributions' => '{{ product.master_id }}', 'id' => 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', + 'ng-disabled' => 'product.variants.length > 0 || !order_cycle.editable_variants_for_incoming_exchanges.hasOwnProperty(exchange.enterprise_id) || order_cycle.editable_variants_for_incoming_exchanges[exchange.enterprise_id].indexOf(product.master_id) < 0' %img{'ng-src' => '{{ product.image_url }}'} {{ product.name }} @@ -17,10 +20,12 @@ -# be able to remove the master variant, since it serves no purpose. Display a checkbox to do so. .exchange-product-variant{'ng-show' => 'exchange.variants[product.master_id] && product.variants.length > 0'} %label - = check_box_tag 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', 1, 1, 'ng-model' => 'exchange.variants[product.master_id]', 'ofn-sync-distributions' => '{{ product.master_id }}', 'id' => 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}' + = check_box_tag 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', 1, 1, 'ng-model' => 'exchange.variants[product.master_id]', 'ofn-sync-distributions' => '{{ product.master_id }}', 'id' => 'order_cycle_incoming_exchange_{{ $parent.$index }}_variants_{{ product.master_id }}', + 'ng-disabled' => '!order_cycle.editable_variants_for_incoming_exchanges.hasOwnProperty(exchange.enterprise_id) || order_cycle.editable_variants_for_incoming_exchanges[exchange.enterprise_id].indexOf(product.master_id) < 0' Obsolete master .exchange-product-variant{'ng-repeat' => 'variant in product.variants'} %label - = check_box_tag 'order_cycle_incoming_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}', 1, 1, 'ng-model' => 'exchange.variants[variant.id]', 'ofn-sync-distributions' => '{{ variant.id }}', 'id' => 'order_cycle_incoming_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}' + = check_box_tag 'order_cycle_incoming_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}', 1, 1, 'ng-model' => 'exchange.variants[variant.id]', 'ofn-sync-distributions' => '{{ variant.id }}', 'id' => 'order_cycle_incoming_exchange_{{ $parent.$parent.$index }}_variants_{{ variant.id }}', + 'ng-disabled' => '!order_cycle.editable_variants_for_incoming_exchanges.hasOwnProperty(exchange.enterprise_id) || order_cycle.editable_variants_for_incoming_exchanges[exchange.enterprise_id].indexOf(variant.id) < 0' {{ variant.label }} diff --git a/app/views/admin/order_cycles/_form.html.haml b/app/views/admin/order_cycles/_form.html.haml index 9aaac195ee..a5f7ec1518 100644 --- a/app/views/admin/order_cycles/_form.html.haml +++ b/app/views/admin/order_cycles/_form.html.haml @@ -1,5 +1,7 @@ = render 'name_and_timing_form', f: f +-if Enterprise.managed_by(spree_current_user).include? @order_cycle.coordinator + = render 'coordinator_fees', f: f %h2 Incoming %table.exchanges @@ -15,15 +17,10 @@ %tr.products{'ng-show' => 'exchange.showProducts'} = render 'exchange_supplied_products_form' -= select_tag :new_supplier_id, options_from_collection_for_select(order_cycle_producer_enterprises, :id, :name), {'ng-model' => 'new_supplier_id'} -= f.submit 'Add supplier', 'ng-click' => 'addSupplier($event)' - - -%h2 Coordinator -= f.label :coordinator_id, 'Coordinator' -= f.collection_select :coordinator_id, order_cycle_coordinating_enterprises, :id, :name, {include_blank: true}, {'ng-model' => 'order_cycle.coordinator_id', 'ofn-on-change' => 'order_cycle.coordinator_fees = []', 'required' => true} -= render 'coordinator_fees', f: f +- if Enterprise.managed_by(spree_current_user).include? @order_cycle.coordinator + = select_tag :new_supplier_id, options_for_select(permitted_producer_enterprise_options_for(@order_cycle)), {'ng-model' => 'new_supplier_id'} + = f.submit 'Add supplier', 'ng-click' => 'addSupplier($event)' %h2 Outgoing %table.exchanges @@ -40,8 +37,9 @@ %tr.products{'ng-show' => 'exchange.showProducts'} = render 'exchange_distributed_products_form' -= select_tag :new_distributor_id, options_for_select(order_cycle_hub_enterprises), {'ng-model' => 'new_distributor_id'} -= f.submit 'Add distributor', 'ng-click' => 'addDistributor($event)' +- if Enterprise.managed_by(spree_current_user).include? @order_cycle.coordinator + = select_tag :new_distributor_id, options_for_select(permitted_hub_enterprise_options_for(@order_cycle)), {'ng-model' => 'new_distributor_id'} + = f.submit 'Add distributor', 'ng-click' => 'addDistributor($event)' .actions = f.submit @order_cycle.new_record? ? 'Create' : 'Update', 'ng-disabled' => '!loaded()' diff --git a/app/views/admin/order_cycles/_name_and_timing_form.html.haml b/app/views/admin/order_cycles/_name_and_timing_form.html.haml index ed24d20f39..703d8094b9 100644 --- a/app/views/admin/order_cycles/_name_and_timing_form.html.haml +++ b/app/views/admin/order_cycles/_name_and_timing_form.html.haml @@ -1,15 +1,28 @@ .row .alpha.two.columns = f.label :name - .fourteen.columns.omega - = f.text_field :name, 'ng-model' => 'order_cycle.name', 'required' => true + .six.columns.omega + - if viewing_as_coordinator_of?(@order_cycle) + = f.text_field :name, 'ng-model' => 'order_cycle.name', 'required' => true + - else + {{ order_cycle.name }} + .two.columns + = f.label :orders_open_at, 'Orders open' + .omega.six.columns + - if viewing_as_coordinator_of?(@order_cycle) + = f.text_field :orders_open_at, 'datetimepicker' => 'order_cycle.orders_open_at', 'ng-model' => 'order_cycle.orders_open_at' + - else + {{ order_cycle.orders_open_at }} .row .alpha.two.columns - = f.label :orders_open_at, 'Orders open' - .six.columns - = f.text_field :orders_open_at, 'datetimepicker' => 'order_cycle.orders_open_at', 'ng-model' => 'order_cycle.orders_open_at' + = f.label :coordinator + .six.columns.omega + = @order_cycle.coordinator.name .two.columns = f.label :orders_close_at, 'Orders close' .six.columns.omega - = f.text_field :orders_close_at, 'datetimepicker' => 'order_cycle.orders_close_at', 'ng-model' => 'order_cycle.orders_close_at' + - if viewing_as_coordinator_of?(@order_cycle) + = f.text_field :orders_close_at, 'datetimepicker' => 'order_cycle.orders_close_at', 'ng-model' => 'order_cycle.orders_close_at' + - else + {{ order_cycle.orders_close_at }} diff --git a/app/views/admin/order_cycles/_row.html.haml b/app/views/admin/order_cycles/_row.html.haml index 882484c091..2594d5c9f3 100644 --- a/app/views/admin/order_cycles/_row.html.haml +++ b/app/views/admin/order_cycles/_row.html.haml @@ -1,29 +1,39 @@ - order_cycle = order_cycle_form.object - klass = "order-cycle-#{order_cycle.id} #{order_cycle_status_class order_cycle}" + %tr{class: klass} %td= link_to order_cycle.name, main_app.edit_admin_order_cycle_path(order_cycle) - %td= order_cycle_form.text_field :orders_open_at, :class => 'datetimepicker', :value => order_cycle.orders_open_at - %td= order_cycle_form.text_field :orders_close_at, :class => 'datetimepicker', :value => order_cycle.orders_close_at + %td= order_cycle_form.text_field :orders_open_at, :class => "#{viewing_as_coordinator_of?(order_cycle) ? 'datetimepicker' : ''}", :value => order_cycle.orders_open_at, :disabled => !viewing_as_coordinator_of?(order_cycle) + %td= order_cycle_form.text_field :orders_close_at, :class => "#{viewing_as_coordinator_of?(order_cycle) ? 'datetimepicker' : ''}", :value => order_cycle.orders_close_at, :disabled => !viewing_as_coordinator_of?(order_cycle) - - unless order_cycles_simple_view + - unless order_cycles_simple_index %td.suppliers - - order_cycle.suppliers.merge(OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_enterprises).each do |s| - = s.name - %br/ + - suppliers = order_cycle.suppliers.merge(OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user, order_cycle).visible_enterprises) + - supplier_list = suppliers.map(&:name).sort.join ', ' + - if suppliers.count > 3 + %span.with-tip{'data-powertip' => supplier_list} + = suppliers.count + suppliers + - else + = supplier_list %td= order_cycle.coordinator.name %td.distributors - - order_cycle.distributors.merge(OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_enterprises).each do |d| - = d.name - %br/ + - distributors = order_cycle.distributors.merge(OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user, order_cycle).visible_enterprises) + - distributor_list = distributors.map(&:name).sort.join ', ' + - if distributors.count > 3 + %span.with-tip{'data-powertip' => distributor_list} + = distributors.count + distributors + - else + = distributor_list %td.products - - variant_images = capture do - - order_cycle.variants.each do |v| - = image_tag(v.images.first.attachment.url(:mini)) if v.images.present? - %br/ - %span.with-tip{'data-powertip' => variant_images}= "#{order_cycle.variants.count} variants" + %span= "#{order_cycle.variants.count} variants" %td.actions = link_to '', main_app.edit_admin_order_cycle_path(order_cycle), class: 'edit-order-cycle icon-edit no-text' %td.actions = link_to '', main_app.clone_admin_order_cycle_path(order_cycle), class: 'clone-order-cycle icon-copy no-text' + - if can_delete?(order_cycle) + %td.actions + = link_to '', main_app.admin_order_cycle_path(order_cycle), class: 'delete-order-cycle icon-trash no-text', :method => :delete, data: { confirm: "Are you sure?" } diff --git a/app/views/admin/order_cycles/edit.html.haml b/app/views/admin/order_cycles/edit.html.haml index d4a6751b48..2f17ecf85b 100644 --- a/app/views/admin/order_cycles/edit.html.haml +++ b/app/views/admin/order_cycles/edit.html.haml @@ -1,9 +1,9 @@ %h1 Edit Order Cycle -- ng_controller = order_cycles_simple_view ? 'AdminSimpleEditOrderCycleCtrl' : 'AdminEditOrderCycleCtrl' +- ng_controller = order_cycles_simple_form ? 'AdminSimpleEditOrderCycleCtrl' : 'AdminEditOrderCycleCtrl' = form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.order_cycles', 'ng-controller' => ng_controller, 'ng-submit' => 'submit($event)'} do |f| - - if order_cycles_simple_view + - if order_cycles_simple_form = render 'simple_form', f: f - else = render 'form', f: f diff --git a/app/views/admin/order_cycles/index.html.haml b/app/views/admin/order_cycles/index.html.haml index aa54f9bff9..e39fdde12a 100644 --- a/app/views/admin/order_cycles/index.html.haml +++ b/app/views/admin/order_cycles/index.html.haml @@ -4,6 +4,12 @@ = content_for :page_actions do %li#new_order_cycle_link = button_link_to "New Order Cycle", main_app.new_admin_order_cycle_path, :icon => 'icon-plus', :id => 'admin_new_order_cycle_link' + - if @show_more + %li + = button_link_to "Show less", main_app.admin_order_cycles_path + - else + %li + = button_link_to "Show more", main_app.admin_order_cycles_path(params: { show_more: true }) = form_for @order_cycle_set, :url => main_app.bulk_update_admin_order_cycles_path do |f| %table.index#listing_order_cycles @@ -11,26 +17,28 @@ %col %col{'style' => 'width: 20%;'} %col{'style' => 'width: 20%;'} - - unless order_cycles_simple_view + - unless order_cycles_simple_index %col %col %col %col %col %col + %col %thead %tr %th Name %th Open %th Close - - unless order_cycles_simple_view - %th Suppliers + - unless order_cycles_simple_index + %th Supplier %th Coordinator %th Distributors %th Products %th.actions %th.actions + %th.actions %tbody = f.fields_for :collection do |order_cycle_form| diff --git a/app/views/admin/order_cycles/new.html.haml b/app/views/admin/order_cycles/new.html.haml index 817f790b19..770eac0269 100644 --- a/app/views/admin/order_cycles/new.html.haml +++ b/app/views/admin/order_cycles/new.html.haml @@ -1,9 +1,10 @@ %h1 New Order Cycle -- ng_controller = order_cycles_simple_view ? 'AdminSimpleCreateOrderCycleCtrl' : 'AdminCreateOrderCycleCtrl' +- ng_controller = order_cycles_simple_form ? 'AdminSimpleCreateOrderCycleCtrl' : 'AdminCreateOrderCycleCtrl' += admin_inject_order_cycle_instance = form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.order_cycles', 'ng-controller' => ng_controller, 'ng-submit' => 'submit($event)'} do |f| - - if order_cycles_simple_view + - if order_cycles_simple_form = render 'simple_form', f: f - else = render 'form', f: f diff --git a/app/views/admin/order_cycles/set_coordinator.html.haml b/app/views/admin/order_cycles/set_coordinator.html.haml new file mode 100644 index 0000000000..f2663cbfd2 --- /dev/null +++ b/app/views/admin/order_cycles/set_coordinator.html.haml @@ -0,0 +1,15 @@ +%h4.text-center Select a coordinator for your order cycle + +%br + += form_for @order_cycle, :url => main_app.new_admin_order_cycle_path, method: :get do |f| + .row + .two.columns.alpha +   + .ten.columns + = select_tag :coordinator_id, options_for_select(permitted_coordinating_enterprise_options_for(@order_cycle)), { 'required' => true, class: 'select2 fullwidth'} + .two.columns.alpha + = f.submit "Continue >" + + .two.columns.omega +   diff --git a/app/views/admin/order_cycles/show.rep b/app/views/admin/order_cycles/show.rep deleted file mode 100644 index fb71602eb6..0000000000 --- a/app/views/admin/order_cycles/show.rep +++ /dev/null @@ -1,28 +0,0 @@ -r.element :order_cycle, @order_cycle do - r.element :id - r.element :name - r.element :orders_open_at, @order_cycle.orders_open_at.to_s - r.element :orders_close_at, @order_cycle.orders_close_at.to_s - - r.element :coordinator_id - r.list_of :coordinator_fees do |fee| - r.element :id - end - - r.list_of :exchanges, OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_exchanges(@order_cycle).order('id ASC') do |exchange| - r.element :id - r.element :sender_id - r.element :receiver_id - r.element :incoming - - r.element :variants, Hash[ exchange.variants.map { |v| [v.id, true] } ], {} - - r.list_of :enterprise_fees do |fee| - r.element :id - r.element :enterprise_id - end - - r.element :pickup_time - r.element :pickup_instructions - end -end diff --git a/app/views/admin/producer_properties/_form.html.haml b/app/views/admin/producer_properties/_form.html.haml new file mode 100644 index 0000000000..dcd83f8ec4 --- /dev/null +++ b/app/views/admin/producer_properties/_form.html.haml @@ -0,0 +1,12 @@ +%fieldset.no-border-top + .add_producer_properties{"data-hook" => "add_producer_properties"} + = image_tag 'spinner.gif', :plugin => 'spree', :style => 'display:none;', :id => 'busy_indicator' + %table.index.sortable{"data-hook" => "", "data-sortable-link" => main_app.update_positions_admin_enterprise_producer_properties_url(@enterprise)} + %thead + %tr{"data-hook" => "producer_properties_header"} + %th{colspan: "2"} Property + %th Value + %th.actions + %tbody#producer_properties{"data-hook" => ""} + = f.fields_for :producer_properties do |pp_form| + = render 'admin/producer_properties/producer_property_fields', f: pp_form diff --git a/app/views/admin/producer_properties/_producer_property_fields.html.haml b/app/views/admin/producer_properties/_producer_property_fields.html.haml index 79dc815f9d..314cbfc31a 100644 --- a/app/views/admin/producer_properties/_producer_property_fields.html.haml +++ b/app/views/admin/producer_properties/_producer_property_fields.html.haml @@ -4,7 +4,10 @@ %span.handle = f.hidden_field :id %td.property_name - = f.text_field :property_name, :class => 'autocomplete' + - if spree_current_user.admin? + = f.text_field :property_name, :class => 'autocomplete' + - else + = f.select :property_name, @properties, { :include_blank => true }, { class: 'select2 fullwidth' } %td.value = f.text_field :value, :class => 'autocomplete' %td.actions diff --git a/app/views/admin/producer_properties/index.html.haml b/app/views/admin/producer_properties/index.html.haml index 2b4277b08d..d8eac9e816 100644 --- a/app/views/admin/producer_properties/index.html.haml +++ b/app/views/admin/producer_properties/index.html.haml @@ -13,20 +13,10 @@ = form_for @enterprise, url: main_app.admin_enterprise_path(@enterprise), method: :put do |f| - %fieldset.no-border-top - .add_producer_properties{"data-hook" => "add_producer_properties"} - = image_tag 'spinner.gif', :plugin => 'spree', :style => 'display:none;', :id => 'busy_indicator' - %table.index.sortable{"data-hook" => "", "data-sortable-link" => main_app.update_positions_admin_enterprise_producer_properties_url(@enterprise)} - %thead - %tr{"data-hook" => "producer_properties_header"} - %th{colspan: "2"} Property - %th Value - %th.actions - %tbody#producer_properties{"data-hook" => ""} - = f.fields_for :producer_properties do |pp_form| - = render 'producer_property_fields', f: pp_form - = render 'spree/admin/shared/edit_resource_links', collection_url: main_app.admin_enterprise_producer_properties_path(@enterprise) - = hidden_field_tag 'clear_producer_properties', 'true' + = render 'form', f: f + + = render 'spree/admin/shared/edit_resource_links', collection_url: main_app.admin_enterprise_producer_properties_path(@enterprise) + = hidden_field_tag 'clear_producer_properties', 'true' :javascript var properties = #{raw(@properties.to_json)}; diff --git a/app/views/admin/enterprises/_side_menu.html.haml b/app/views/admin/shared/_side_menu.html.haml similarity index 69% rename from app/views/admin/enterprises/_side_menu.html.haml rename to app/views/admin/shared/_side_menu.html.haml index ff5d086570..cf1d3d42f7 100644 --- a/app/views/admin/enterprises/_side_menu.html.haml +++ b/app/views/admin/shared/_side_menu.html.haml @@ -2,9 +2,7 @@ %a.menu_item{ href: "", id: "{{ item.name.toLowerCase().replace(' ', '_') }}", ng: { repeat: '(index,item) in menu.items | filter:{visible:true}', click: 'select(index)', - show: 'showItem(item)', - class: '{ selected: item.selected}', - 'class-odd' => "'odd'", - 'class-even' => "'even'" } } + show: '!showItem || showItem(item)', + class: '{ selected: item.selected }' } } %i{ class: "{{item.icon_class}}" } %span {{ item.name }} diff --git a/app/views/checkout/_accordion_heading.html.haml b/app/views/checkout/_accordion_heading.html.haml index 76724cd29e..c7872ce109 100644 --- a/app/views/checkout/_accordion_heading.html.haml +++ b/app/views/checkout/_accordion_heading.html.haml @@ -1,10 +1,10 @@ %accordion-heading .row - .small-8.medium-10.columns + .small-8.medium-9.columns %em %small {{ summary() | printArray }} - .small-4.medium-2.columns.text-right + .small-4.medium-3.columns.text-right %span.accordion-up %em %small Hide diff --git a/app/views/checkout/_payment.html.haml b/app/views/checkout/_payment.html.haml index 36775d6d35..cd3091efcd 100644 --- a/app/views/checkout/_payment.html.haml +++ b/app/views/checkout/_payment.html.haml @@ -35,4 +35,4 @@ = render partial: "spree/checkout/payment/#{method.method_type}", :locals => { :payment_method => method } .small-12.columns.medium-6.columns.large-6.columns #distributor_address.panel{"ng-show" => "Checkout.paymentMethod().description"} - %span{ style: "white-space: pre-wrap;" }{{ Checkout.paymentMethod().description }} + %span.pre-wrap {{ Checkout.paymentMethod().description }} diff --git a/app/views/checkout/_shipping.html.haml b/app/views/checkout/_shipping.html.haml index ffb1e43de4..e769376107 100644 --- a/app/views/checkout/_shipping.html.haml +++ b/app/views/checkout/_shipping.html.haml @@ -21,6 +21,10 @@ "ng-value" => "method.id", "ng-model" => "order.shipping_method_id"} {{ method.name }} + %em.light{"ng-show" => "!method.price || method.price == 0"} + (Free) + %em.light{"ng-hide" => "!method.price || method.price == 0"} + ({{ method.price | localizeCurrency }}) %small.error.medium.input-text{"ng-show" => "!fieldValid('order.shipping_method_id')"} = "{{ fieldErrors('order.shipping_method_id') }}" @@ -42,7 +46,7 @@ .row .small-12.columns - = f.text_area :special_instructions, label: "Any notes or custom delivery instructions?", size: "60x4", "ng-model" => "order.special_instructions" + = f.text_area :special_instructions, label: "Any comments or special instructions?", size: "60x4", "ng-model" => "order.special_instructions" .row .small-12.columns.text-right diff --git a/app/views/checkout/edit.html.haml b/app/views/checkout/edit.html.haml index 2744627d03..2f30c93ba5 100644 --- a/app/views/checkout/edit.html.haml +++ b/app/views/checkout/edit.html.haml @@ -16,7 +16,7 @@ .small-12.medium-8.large-9.columns - unless spree_current_user = render partial: "checkout/authentication" - .row{"ng-show" => "enabled", "ng-controller" => "AccordionCtrl"} + %div{"ng-show" => "enabled", "ng-controller" => "AccordionCtrl"} = render partial: "checkout/form" .small-12.medium-4.large-3.columns = render partial: "checkout/summary" diff --git a/app/views/groups/_contact.html.haml b/app/views/groups/_contact.html.haml new file mode 100644 index 0000000000..915c608d15 --- /dev/null +++ b/app/views/groups/_contact.html.haml @@ -0,0 +1,47 @@ +%div.contact-container{bindonce: true} + - if @group.email.present? || @group.website.present? || @group.phone.present? + %div.modal-centered + %p.modal-header Contact + - if @group.phone.present? + %p + %a{tel: @group.phone} + = @group.phone + - if @group.email.present? + %p + =link_to_service "", @group.email.reverse, mailto: true do + Email us + - if @group.website.present? + %p + =link_to_service "http://", @group.website do + Visit our website + +%div{bindonce: true} + - if @group.facebook.present? || @group.twitter.present? || @group.linkedin.present? || @group.instagram.present? + %div.modal-centered.pad-top + %p.modal-header Follow + .follow-icons{bindonce: true} + =link_to_service "http://twitter.com/", @group.twitter do + %i.ofn-i_041-twitter + =link_to_service "https://www.facebook.com/", @group.facebook do + %i.ofn-i_044-facebook + =link_to_service "https://www.linkedin.com/in/", @group.linkedin do + %i.ofn-i_042-linkedin + =link_to_service "http://instagram.com/", @group.instagram do + %i.ofn-i_043-instagram + +%div{bindonce: true} + - if @group.address1.present? || @group.city.present? + %div.modal-centered.pad-top + %p.modal-header Address + %p + = @group.address1 + - if @group.address2.present? + %br + = @group.address2 + - if @group.city.present? + %br + = @group.city + = @group.state + = @group.zipcode + + diff --git a/app/views/groups/index.html.haml b/app/views/groups/index.html.haml index 1aa5b02629..262e188e3c 100644 --- a/app/views/groups/index.html.haml +++ b/app/views/groups/index.html.haml @@ -3,52 +3,38 @@ :javascript angular.module('Darkswarm').value('groups', #{render partial: "json/groups", object: @groups}) -#groups{"ng-controller" => "GroupsCtrl"} +#groups.pad-top{"ng-controller" => "GroupsCtrl"} #active-table-search.row.pad-top - .small-12.columns.text-center - %h1 Groups / Regions - %div - Check out our - %ofn-modal{title: "food groups"} - = render partial: "modals/groups" - below + .small-12.columns + %h1 Groups / regions %p - - %input.animate-show{type: :text, - "ng-model" => "query", - placeholder: "Search group name", - "ng-debounce" => "150", - "ofn-disable-enter" => true} + %input.animate-show{type: :text, + "ng-model" => "query", + placeholder: "Search name or keyword", + "ng-debounce" => "150", + "ofn-disable-enter" => true} .group{"ng-repeat" => "group in groups = (Groups.groups | groups:query | orderBy:order)", name: "group{{group.id}}", id: "group{{group.id}}"} .row.pad-top{bindonce: true} - .small-12.columns - .group-hero - %img.group-hero-img{"bo-src" => "group.promo_image"} - %img.group-logo{"bo-src" => "group.logo", "bo-if" => "group.logo"} - %h3.group-name + .small-12.medium-6.columns + .groups-header + %a{"ng-href" => "/groups/{{group.id}}"} %i.ofn-i_035-groups - {{ group.name }} - %h5.group-description {{ group.description }} - - .row.pad-top{bindonce: true} - .small-6.columns - %p {{ group.long_description }} - .small-6.columns - %h5 Our hubs & producers - %ul.small-block-grid-2 - %li{"ng-repeat" => "enterprise in group.enterprises", "scroll-after-load" => true} - %enterprise-modal{"ng-if" => "enterprise.is_distributor"} - {{ enterprise.name }} - %enterprise-modal{"ng-if" => "!enterprise.is_distributor", "show-hub-actions" => 'true'} - {{ enterprise.name }} - - - .row.group_footer - .small-12.columns - %hr + %span.group-name + {{ group.name }} + .small-3.medium-2.columns + %p + {{ group.state }} + .small-9.medium-4.columns.groups-icons + %p + %link-to-service.ofn-i_050-mail-circle{service: '""', ref: 'group.email.split("").reverse().join("")', mailto: true} + %link-to-service.ofn-i_049-web{service: '"http://"', ref: 'group.website'} + %link-to-service.ofn-i_041-twitter{service: '"http://twitter.com/"', ref: 'group.twitter'} + %link-to-service.ofn-i_044-facebook{service: '"https://www.facebook.com/"', ref: 'group.facebook'} + %link-to-service.ofn-i_042-linkedin{service: '"https://www.linkedin.com/in/"', ref: 'group.linkedin'} + %link-to-service.ofn-i_043-instagram{service: '"http://instagram.com/"', ref: 'group.instagram'} .group{"ng-show" => "groups.length == 0"} .row.pad-top diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml new file mode 100644 index 0000000000..1bc965dfb3 --- /dev/null +++ b/app/views/groups/show.html.haml @@ -0,0 +1,118 @@ +-# inject all enterprises as "enterprises" +-# it could be more efficient to inject only the enterprises that are related to the group += inject_enterprises + +-# inject enterprises in this group +-# further hubs and producers of these enterprises can't be resoleved within this small subset += inject_json_ams "group_enterprises", @group.enterprises, Api::EnterpriseSerializer, active_distributors: @active_distributors + +#group-page.row.pad-top{"ng-controller" => "GroupPageCtrl"} + .small-12.columns.pad-top + %header + .row + .small-12.columns + - if @group.promo_image.present? + %img{"src" => @group.promo_image} + .row + .small-12.columns.group-header.pad-top + - if @group.logo.present? + %img.group-logo{"src" => @group.logo} + - else + %img.group-logo{"src" => '/assets/noimage/group.png'} + %h2.group-name= @group.name + %p= @group.description + + .small-12.columns.pad-top + .row + .small-12.medium-12.large-9.columns + %div{"ng-controller" => "TabsCtrl"} + %tabset + %tab{heading: 'Map', + active: "active(\'\')", + select: "select(\'\')"} + .map-container + %map{"ng-if" => "(active(\'\') && (mapShowed = true)) || mapShowed"} + %google-map{options: "map.additional_options", center: "map.center", zoom: "map.zoom", styles: "map.styles", draggable: "true"} + %markers{models: "mapMarkers", fit: "true", + coords: "'self'", icon: "'icon'", click: "'reveal'"} + + %tab{heading: 'About us', + active: "active(\'about\')", + select: "select(\'about\')"} + %h1 About Us + %p!= @group.long_description + + %tab{heading: 'Our producers', + active: "active(\'producers\')", + select: "select(\'producers\')"} + .producers{"ng-controller" => "GroupEnterprisesCtrl"} + .row + .small-12.columns + %h1 Our Producers + = render partial: "shared/components/enterprise_search" + -# TODO: find out why this is not working + -#= render partial: "producers/filters" + + .row{bindonce: true} + .small-12.columns + .active_table + %producer.active_table_node.row.animate-repeat{id: "{{producer.path}}", + "ng-repeat" => "producer in filteredEnterprises = (group_producers | visible | searchEnterprises:query | taxons:activeTaxons)", + "ng-controller" => "GroupEnterpriseNodeCtrl", + "ng-class" => "{'closed' : !open(), 'open' : open(), 'inactive' : !producer.active}", + id: "{{producer.hash}}"} + + .small-12.columns + = render partial: 'producers/skinny' + = render partial: 'producers/fat' + + = render partial: 'shared/components/enterprise_no_results' + + %tab{heading: 'Our hubs', + active: "active(\'hubs\')", + select: "select(\'hubs\')"} + .hubs{"ng-controller" => "GroupEnterprisesCtrl"} + .row + .small-12.columns + %h1 Our Hubs + + = render partial: "shared/components/enterprise_search" + -# TODO: find out why this is not working + -#= render partial: "home/filters" + .small-12.medium-6.columns + %span   + = render partial: 'shared/components/show_profiles' + + .row{bindonce: true} + .small-12.columns + .active_table + %hub.active_table_node.row.animate-repeat{id: "{{hub.hash}}", + "ng-repeat" => "hub in filteredEnterprises = (group_hubs | visible | searchEnterprises:query | taxons:activeTaxons | shipping:shippingTypes | showHubProfiles:show_profiles | orderBy:['-active', '+orders_close_at'])", + "ng-class" => "{'is_profile' : hub.category == 'hub_profile', 'closed' : !open(), 'open' : open(), 'inactive' : !hub.active, 'current' : current()}", + "ng-controller" => "GroupEnterpriseNodeCtrl"} + .small-12.columns + = render partial: 'home/skinny' + = render partial: 'home/fat' + + = render partial: 'shared/components/enterprise_no_results' + + .small-12.medium-12.large-3.columns + = render partial: 'contact' + + .small-12.columns.pad-top + .row.pad-top + .small-12.columns.text-center.small + %hr + %p.text-small + = "Copyright #{Date.today.year} #{@group.name}" + %h2 + =link_to_service "https://www.facebook.com/", @group.facebook, title: 'Follow us on Facebook' do + %i.ofn-i_044-facebook + =link_to_service "", @group.email.reverse, title:'Email us', mailto: true do + %i.ofn-i_050-mail-circle + =link_to_service "http://", @group.website, title: 'Visit our website' do + %i.ofn-i_049-web + %p +   + += render partial: "shared/footer" diff --git a/app/views/json/_groups.rabl b/app/views/json/_groups.rabl index 691d36f42f..a079290e23 100644 --- a/app/views/json/_groups.rabl +++ b/app/views/json/_groups.rabl @@ -1,5 +1,5 @@ collection @groups -attributes :id, :name, :position, :description, :long_description +attributes :id, :name, :position, :description, :long_description, :email, :website, :facebook, :instagram, :linkedin, :twitter child enterprises: :enterprises do attributes :id @@ -12,3 +12,7 @@ end node :promo_image do |group| group.promo_image(:large) if group.promo_image.exists? end + +node :state do |group| + group.state().andand.abbr +end diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml index ef8d4e2ec1..9042c91213 100644 --- a/app/views/layouts/mailer.html.haml +++ b/app/views/layouts/mailer.html.haml @@ -7,15 +7,15 @@ Open Food Network = stylesheet_link_tag 'mail/all' %body{:bgcolor => "#FFFFFF" } - %table.head-wrap{:bgcolor => "#333333"} + %table.head-wrap{:bgcolor => "#f2f2f2"} %tr %td %td.header.container .content - %table{:bgcolor => "#333333"} + %table{:bgcolor => "#f2f2f2"} %tr %td - %img{:src => "#{ asset_path 'open-food-network-beta.png' }", :width => "200"}/ + %img{:src => "#{ asset_path 'open-food-network-beta-black.png' }", :width => "200", :height => "49"}/ %td{:align => "right"} %h6.collapse Open Food Network diff --git a/app/views/producers/_fat.html.haml b/app/views/producers/_fat.html.haml index f420b33386..17304abbe3 100644 --- a/app/views/producers/_fat.html.haml +++ b/app/views/producers/_fat.html.haml @@ -4,7 +4,7 @@ / Will add in long description available once clean up HTML formatting producer.long_description %div{"bo-if" => "producer.description"} %label About us - %img.right.show-for-medium-up{src: "{{ producer.logo }}" } + %img.right.show-for-medium-up{"ng-src" => "{{producer.logo}}" } %p.text-small {{ producer.description }} %div.show-for-medium-up{"bo-if" => "producer.description.length==0"} diff --git a/app/views/registration/index.html.haml b/app/views/registration/index.html.haml index de09d9494f..d110e98657 100644 --- a/app/views/registration/index.html.haml +++ b/app/views/registration/index.html.haml @@ -1,4 +1,5 @@ -=inject_spree_api_key -=inject_available_countries -=inject_enterprise_attributes -%div{ "ng-controller" => "RegistrationCtrl" } \ No newline at end of file += inject_spree_api_key += inject_available_countries += inject_enterprise_attributes + +%div{ "ng-controller" => "RegistrationCtrl" } diff --git a/app/views/shared/_footer.html.haml b/app/views/shared/_footer.html.haml index 70eb14dd41..e06ae0f0aa 100644 --- a/app/views/shared/_footer.html.haml +++ b/app/views/shared/_footer.html.haml @@ -43,7 +43,9 @@ %h5.pad-top %a{title: 'Open Food Network', href:'http://www.openfoodnetwork.org', target: '_blank' } openfoodnetwork.org %br - © Copyright 2014 Open Food Foundation + © Copyright + = Date.today.year + Open Food Foundation %p %small %a{href:"https://creativecommons.org/licenses/by-sa/3.0/", target: "_blank" } Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) diff --git a/app/views/shared/_signed_in.html.haml b/app/views/shared/_signed_in.html.haml index 0d952dc4a8..69f0e61dbd 100644 --- a/app/views/shared/_signed_in.html.haml +++ b/app/views/shared/_signed_in.html.haml @@ -7,14 +7,15 @@ - if admin_user? or enterprise_user? %li - %a{href: spree.admin_path} + %a{href: spree.admin_path, target:'_blank'} %i.ofn-i_021-tools - Admin + Administration %li %a{href: spree.account_path} %i.ofn-i_015-user - = spree_current_user.email + Account + = "(" + spree_current_user.email + ")" %li %a{title: 'Log Out', href:'/logout' } diff --git a/app/views/shared/_signed_in_offcanvas.html.haml b/app/views/shared/_signed_in_offcanvas.html.haml index c063a885fe..97b3e1ebca 100644 --- a/app/views/shared/_signed_in_offcanvas.html.haml +++ b/app/views/shared/_signed_in_offcanvas.html.haml @@ -1,13 +1,14 @@ - if admin_user? or enterprise_user? %li - %a{href: spree.admin_path} + %a{href: spree.admin_path, target:'_blank'} %i.ofn-i_021-tools Admin %li %a{href: spree.account_path} %i.ofn-i_015-user - = spree_current_user.email + Account + / = spree_current_user.email %li %a{title: 'Log Out', href:'/logout' } diff --git a/app/views/shared/menu/_cart.html.haml b/app/views/shared/menu/_cart.html.haml index aa7f7940c9..f8287c8f61 100644 --- a/app/views/shared/menu/_cart.html.haml +++ b/app/views/shared/menu/_cart.html.haml @@ -15,8 +15,8 @@ "ng-controller" => "LineItemCtrl", "id" => "cart-variant-{{ line_item.variant.id }}"} %td %small - %strong {{ line_item.variant.name_to_display }} - %em {{ line_item.variant.unit_to_display }} + %strong + {{ line_item.variant.extended_name }} %td.text-right %small %span.quantity {{ line_item.quantity }} diff --git a/app/views/shopping_shared/_tabs.html.haml b/app/views/shopping_shared/_tabs.html.haml index b654366ecc..3641e0cbd5 100644 --- a/app/views/shopping_shared/_tabs.html.haml +++ b/app/views/shopping_shared/_tabs.html.haml @@ -11,6 +11,6 @@ %tab.columns{heading: heading, id: "tab_#{name}", active: "active(\'#{name}\')", - select: "select(\'#{name}\')", + select: "toggle(\'#{name}\')", class: "small-12 medium-#{cols}" } = render "shopping_shared/#{name}" diff --git a/app/views/spree/admin/products/_tax_category_form.html.haml b/app/views/spree/admin/products/_tax_category_form.html.haml index 81a8677528..3d529e6562 100644 --- a/app/views/spree/admin/products/_tax_category_form.html.haml +++ b/app/views/spree/admin/products/_tax_category_form.html.haml @@ -2,5 +2,5 @@ = f.label :tax_category_id, t(:tax_category) %span.required * %br - = f.collection_select(:tax_category_id, Spree::TaxCategory.all, :id, :name, {:include_blank => Spree::TaxCategory.count > 1}, {:class => "select2 fullwidth"}) + = f.collection_select(:tax_category_id, Spree::TaxCategory.all, :id, :name, {:include_blank => Spree::Config.products_require_tax_category ? false : 'None'}, {:class => "select2 fullwidth"}) = f.error_message_on :tax_category_id diff --git a/app/views/spree/admin/reports/sales_tax.html.haml b/app/views/spree/admin/reports/sales_tax.html.haml new file mode 100644 index 0000000000..a7b3d9275d --- /dev/null +++ b/app/views/spree/admin/reports/sales_tax.html.haml @@ -0,0 +1,32 @@ += form_for @search, :url => spree.sales_tax_admin_reports_path do |f| + = render 'date_range_form', f: f + + .row + .four.columns.alpha + = label_tag nil, "Distributor:" + = f.collection_select(:distributor_id_eq, @distributors, :id, :name, {:include_blank => 'All'}, {:class => "select2 fullwidth"}) + = check_box_tag :csv + = label_tag :csv, "Download as csv" + %br + = button t(:search) + +%br +%br +%table#listing_orders.index + %thead + %tr{'data-hook' => "orders_header"} + - @report.header.each do |heading| + %th= heading + %tbody + - @report.table.each do |row| + %tr + - row.each_with_index do |column, i| + - if i == 0 + %td + %a.edit-order{'href' => "/admin/orders/#{column}"}= column + - else + %td= column + - if @report.table.empty? + %tr + %td{:colspan => @report.header.count}= t(:none) + diff --git a/app/views/spree/layouts/admin/_login_nav.html.haml b/app/views/spree/layouts/admin/_login_nav.html.haml index 4ecb72d148..088ac02377 100644 --- a/app/views/spree/layouts/admin/_login_nav.html.haml +++ b/app/views/spree/layouts/admin/_login_nav.html.haml @@ -5,7 +5,7 @@ \: #{spree_current_user.email} %li{"data-hook" => "user-account-link"} %i.icon-user - = link_to t(:account), spree.edit_user_path(spree_current_user) + = link_to t(:account), account_path %li{"data-hook" => "user-logout-link"} %i.icon-signout = link_to t(:logout), spree.logout_path diff --git a/app/views/spree/order_mailer/confirm_email_for_customer.html.haml b/app/views/spree/order_mailer/confirm_email_for_customer.html.haml index 69629e256b..4488c76edb 100644 --- a/app/views/spree/order_mailer/confirm_email_for_customer.html.haml +++ b/app/views/spree/order_mailer/confirm_email_for_customer.html.haml @@ -1,150 +1,172 @@ -%h3 Hi #{@order.bill_address.firstname}, %table.social.white-bg{:width => "100%"} %tr %td %table.column{:align => "left"} %tr %td - %p - %strong Order confirmation ##{@order.number} - %p - Thanks for shopping on - %strong= "#{Spree::Config.site_name}." - Here are the details for your order from - %strong= "#{@order.distributor.name}." + %h3 + Hi #{@order.bill_address.firstname}, + %h4 + Thanks for shopping at + %strong= "#{@order.distributor.name}!" %table.column{:align => "left"} %tr - %td - %img.float-right{:src => "#{@order.distributor.logo.url(:thumb)}"}/ + %td{:align => "right"} + %img.float-right{:src => "#{@order.distributor.logo.url(:medium)}"}/ %span.clear + %p   +%h4 + Order confirmation + %strong ##{@order.number} +%p + Here are your order details from + %strong= "#{@order.distributor.name}:" - -%p.callout - %strong Order summary %table.order-summary{:width => "100%"} - %tr - %td - %strong Item - %td{:align => "right", :width => "25%"} - %strong Qty - %td{:align => "right", :width => "25%"} - %strong Price - - @order.line_items.each do |item| + %thead %tr - %td - = raw(item.variant.product.supplier.name) - %br - = "#{raw(item.variant.product.name)} #{raw(item.variant.options_text)}" - %td{:align => "right"} - = item.quantity - %td{:align => "right"} - = item.display_amount_with_adjustments - - %tr - %td{:colspan => "3"}   - %tr - %td{:align => "right", :colspan => "2"} - Subtotal: - %td{:align => "right"} - = display_checkout_subtotal(@order) - - checkout_adjustments_for(@order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| + %th{:align => "left"} + %h4 Item + %th{:align => "right", :width => "25%"} + %h4 Qty + %th{:align => "right", :width => "25%"} + %h4 Price + %tbody + - @order.line_items.each do |item| + %tr + %td + - if item.variant.product.name == item.variant.name_to_display + %strong= "#{raw(item.variant.product.name)}" + - else + %strong + %span= "#{raw(item.variant.product.name)}" + %span= "- " + "#{raw(item.variant.name_to_display)}" + - if item.variant.options_text + = "(" + "#{raw(item.variant.options_text)}" + ")" + %br + %small + %em= raw(item.variant.product.supplier.name) + %td{:align => "right"} + = item.quantity + %td{:align => "right"} + = item.display_amount_with_adjustments + %tfoot %tr %td{:align => "right", :colspan => "2"} - = "#{raw(adjustment.label)}:" + Subtotal: %td{:align => "right"} - = adjustment.display_amount - %tr - %td{:align => "right", :colspan => "2"} - %strong Total: - %td{:align => "right"} - %strong= @order.display_total + = display_checkout_subtotal(@order) + - checkout_adjustments_for(@order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| + %tr + %td{:align => "right", :colspan => "2"} + = "#{raw(adjustment.label)}:" + %td{:align => "right"} + = adjustment.display_amount + %tr + %td{:align => "right", :colspan => "2"} + %strong Total: + %td{:align => "right"} + %strong= @order.display_total %p   - if @order.payments.first.andand.payment_method.andand.type == "Spree::PaymentMethod::Check" and @order.payments.first.andand.payment_method.andand.description %p.callout + %span{:style => "float:right;"} + - if @order.paid? + PAID + - else + NOT PAID %strong Payment summary - / /Heading Panel - / Payment Summary - %p= @order.payments.first.andand.payment_method.andand.description.andand.html_safe + %h4 + Paying via: + %strong= @order.payments.first.andand.payment_method.andand.name.andand.html_safe + %p + %em= @order.payments.first.andand.payment_method.andand.description.andand.html_safe %p   - if @order.shipping_method.andand.require_ship_address / Delivery details %p.callout %strong - Delivery details - %p - Your order will be delivered to: - %br - #{@order.ship_address.full_name} - %br - #{@order.ship_address.full_address} - %br - #{@order.ship_address.phone} + - if @order.shipping_method.andand.name + #{@order.shipping_method.name.html_safe} + - else + Delivery details + - if @order.order_cycle.andand.pickup_time_for(@order.distributor) + %h4 + Delivery on: + %strong #{@order.order_cycle.pickup_time_for(@order.distributor)} - if @order.shipping_method.andand.description + %p + %em #{@order.shipping_method.description.html_safe} + %br   + + - if @order.ship_address + %h4 Delivery address: + %p + #{@order.ship_address.full_name} %br + #{@order.ship_address.full_address} %br - #{@order.shipping_method.description.html_safe} + #{@order.ship_address.phone} + %br   - - if @order.order_cycle.andand.pickup_time_for(@order.distributor) - %br - %br - Delivery on: #{@order.order_cycle.pickup_time_for(@order.distributor)} - - - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) - %br - %br - Other delivery information: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} - - %br - else / Collection details %p.callout %strong - Collection details + - if @order.shipping_method.andand.name + #{@order.shipping_method.name.html_safe} + - else + Collection details + + - if @order.order_cycle.andand.pickup_time_for(@order.distributor).present? + %h4 + Ready for collection: + %strong #{@order.order_cycle.pickup_time_for(@order.distributor)} + + - if @order.shipping_method.andand.description.present? + %p + %em #{@order.shipping_method.description.html_safe} + %br   + + - if @order.ship_address.full_address + %p + %strong Collecting from: + %br + #{@order.ship_address.full_address} + + - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor).present? + %p + %strong Collection instructions: + %br + #{@order.order_cycle.pickup_instructions_for(@order.distributor)} + +- if @order.special_instructions.present? + %br %p - - if @order.shipping_method.andand.description + %small + %strong Your notes: %br - %br - = @order.shipping_method.description.html_safe - - - if @order.order_cycle.andand.pickup_time_for(@order.distributor) - %br - %br - Ready for collection: #{@order.order_cycle.pickup_time_for(@order.distributor)} - - - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) - %br - %br - Collection instructions: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} - - - if @order.special_instructions.present? - %br - %br - Notes: #{@order.special_instructions} - - %br - - -# Your order will be ready for collection on - -# %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - -# Tuesday 07 Dec - -# %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} - -# Pick-up your order at the rear of 34 Mason Street, Warragul. See it on - -# %a{:href => "https://goo.gl/maps/T1ArU", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} - -# google maps - + #{@order.special_instructions} +%br %p.callout - #{@order.distributor.contact}, - %br/ - %strong= @order.distributor.name - %br/ + Kind regards, + %br + #{@order.distributor.contact} + %br + %br + = @order.distributor.name + %br = @order.distributor.phone || "" - %br/ + %br %a{:href => "mailto:#{@order.distributor.email}", :target => "_blank"} = @order.distributor.email - %br/ - = @order.distributor.website || "" \ No newline at end of file + %br + = @order.distributor.website || "" + + diff --git a/app/views/spree/order_mailer/confirm_email_for_shop.html.haml b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml index 69629e256b..fe48c1c6d1 100644 --- a/app/views/spree/order_mailer/confirm_email_for_shop.html.haml +++ b/app/views/spree/order_mailer/confirm_email_for_shop.html.haml @@ -1,150 +1,157 @@ -%h3 Hi #{@order.bill_address.firstname}, %table.social.white-bg{:width => "100%"} %tr %td %table.column{:align => "left"} %tr %td - %p - %strong Order confirmation ##{@order.number} - %p - Thanks for shopping on - %strong= "#{Spree::Config.site_name}." - Here are the details for your order from - %strong= "#{@order.distributor.name}." + %h3 + Hi #{@order.distributor.contact}, + %h4 + Well done! You have a new order for + %strong= "#{@order.distributor.name}!" %table.column{:align => "left"} %tr - %td - %img.float-right{:src => "#{@order.distributor.logo.url(:thumb)}"}/ + %td{:align => "right"} + %img.float-right{:src => "#{@order.distributor.logo.url(:medium)}"}/ %span.clear + %p   +%h4 + Order confirmation + %strong ##{@order.number} +%p + %strong= "#{@order.bill_address.firstname} #{@order.bill_address.lastname}" + completed the following order at your shopfront: - -%p.callout - %strong Order summary %table.order-summary{:width => "100%"} - %tr - %td - %strong Item - %td{:align => "right", :width => "25%"} - %strong Qty - %td{:align => "right", :width => "25%"} - %strong Price - - @order.line_items.each do |item| + %thead %tr - %td - = raw(item.variant.product.supplier.name) - %br - = "#{raw(item.variant.product.name)} #{raw(item.variant.options_text)}" - %td{:align => "right"} - = item.quantity - %td{:align => "right"} - = item.display_amount_with_adjustments - - %tr - %td{:colspan => "3"}   - %tr - %td{:align => "right", :colspan => "2"} - Subtotal: - %td{:align => "right"} - = display_checkout_subtotal(@order) - - checkout_adjustments_for(@order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| + %th{:align => "left"} + %h4 Item + %th{:align => "right", :width => "25%"} + %h4 Qty + %th{:align => "right", :width => "25%"} + %h4 Price + %tbody + - @order.line_items.each do |item| + %tr + %td + - if item.variant.product.name == item.variant.name_to_display + %strong= "#{raw(item.variant.product.name)}" + - else + %strong + %span= "#{raw(item.variant.product.name)}" + %span= "- " + "#{raw(item.variant.name_to_display)}" + - if item.variant.options_text + = "(" + "#{raw(item.variant.options_text)}" + ")" + %br + %small + %em= raw(item.variant.product.supplier.name) + %td{:align => "right"} + = item.quantity + %td{:align => "right"} + = item.display_amount_with_adjustments + %tfoot %tr %td{:align => "right", :colspan => "2"} - = "#{raw(adjustment.label)}:" + Subtotal: %td{:align => "right"} - = adjustment.display_amount - %tr - %td{:align => "right", :colspan => "2"} - %strong Total: - %td{:align => "right"} - %strong= @order.display_total + = display_checkout_subtotal(@order) + - checkout_adjustments_for(@order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| + %tr + %td{:align => "right", :colspan => "2"} + = "#{raw(adjustment.label)}:" + %td{:align => "right"} + = adjustment.display_amount + %tr + %td{:align => "right", :colspan => "2"} + %strong Total: + %td{:align => "right"} + %strong= @order.display_total %p   - if @order.payments.first.andand.payment_method.andand.type == "Spree::PaymentMethod::Check" and @order.payments.first.andand.payment_method.andand.description %p.callout + %span{:style => "float:right;"} + - if @order.paid? + PAID + - else + NOT PAID %strong Payment summary - / /Heading Panel - / Payment Summary - %p= @order.payments.first.andand.payment_method.andand.description.andand.html_safe + %h4 + Paying via: + %strong= @order.payments.first.andand.payment_method.andand.name.andand.html_safe + %p + %em= @order.payments.first.andand.payment_method.andand.description.andand.html_safe %p   - if @order.shipping_method.andand.require_ship_address / Delivery details %p.callout %strong - Delivery details - %p - Your order will be delivered to: - %br - #{@order.ship_address.full_name} - %br - #{@order.ship_address.full_address} - %br - #{@order.ship_address.phone} + - if @order.shipping_method.andand.name + #{@order.shipping_method.name.html_safe} + - else + Delivery details + - if @order.order_cycle.andand.pickup_time_for(@order.distributor) + %h4 + Delivery on: + %strong #{@order.order_cycle.pickup_time_for(@order.distributor)} - if @order.shipping_method.andand.description + %p + %em #{@order.shipping_method.description.html_safe} + %br   + + - if @order.ship_address + %h4 Delivery address: + %p + #{@order.ship_address.full_name} %br + #{@order.ship_address.full_address} %br - #{@order.shipping_method.description.html_safe} - - - if @order.order_cycle.andand.pickup_time_for(@order.distributor) - %br - %br - Delivery on: #{@order.order_cycle.pickup_time_for(@order.distributor)} - - - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) - %br - %br - Other delivery information: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} - - %br + #{@order.ship_address.phone} + %br   - else / Collection details %p.callout %strong - Collection details + - if @order.shipping_method.andand.name + #{@order.shipping_method.name.html_safe} + - else + Collection details + + - if @order.order_cycle.andand.pickup_time_for(@order.distributor).present? + %h4 + Ready for collection: + %strong #{@order.order_cycle.pickup_time_for(@order.distributor)} + + - if @order.shipping_method.andand.description.present? + %p + %em #{@order.shipping_method.description.html_safe} + %br   + + - if @order.ship_address.full_address + %p + %strong Collecting from: + %br + #{@order.ship_address.full_address} + + - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor).present? + %p + %strong Collection instructions: + %br + #{@order.order_cycle.pickup_instructions_for(@order.distributor)} + +- if @order.special_instructions.present? + %br %p - - if @order.shipping_method.andand.description + %small + %strong Customer notes: %br - %br - = @order.shipping_method.description.html_safe + #{@order.special_instructions} - - if @order.order_cycle.andand.pickup_time_for(@order.distributor) - %br - %br - Ready for collection: #{@order.order_cycle.pickup_time_for(@order.distributor)} - - - if @order.order_cycle.andand.pickup_instructions_for(@order.distributor) - %br - %br - Collection instructions: #{@order.order_cycle.pickup_instructions_for(@order.distributor)} - - - if @order.special_instructions.present? - %br - %br - Notes: #{@order.special_instructions} - - %br - - -# Your order will be ready for collection on - -# %strong{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;"} - -# Tuesday 07 Dec - -# %p{:style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;margin-bottom: 10px;font-weight: normal;font-size: 14px;line-height: 1.6;"} - -# Pick-up your order at the rear of 34 Mason Street, Warragul. See it on - -# %a{:href => "https://goo.gl/maps/T1ArU", :style => "margin: 0;padding: 0;font-family: \"Helvetica Neue\", \"Helvetica\", Helvetica, Arial, sans-serif;color: #0096ad;", :target => "_blank"} - -# google maps - - -%p.callout - #{@order.distributor.contact}, - %br/ - %strong= @order.distributor.name - %br/ - = @order.distributor.phone || "" - %br/ - %a{:href => "mailto:#{@order.distributor.email}", :target => "_blank"} - = @order.distributor.email - %br/ - = @order.distributor.website || "" \ No newline at end of file +%p   += render 'shared/mailers/signoff' += render 'shared/mailers/social_and_contact' diff --git a/app/views/spree/orders/_adjustments.html.haml b/app/views/spree/orders/_adjustments.html.haml index 71579e3ace..12b16375ec 100644 --- a/app/views/spree/orders/_adjustments.html.haml +++ b/app/views/spree/orders/_adjustments.html.haml @@ -1,11 +1,9 @@ -%thead - %tr{"data-hook" => "cart_adjustments_headers"} - %th.cart-adjustment-header{colspan: "6"} - %a{ href: "#" } Fees... +%tr{"data-hook" => "cart_adjustments_headers"} + %td.cart-adjustments{colspan: "5"} + %a{ href: "#" } Fees... -%tbody#cart_adjustments{"data-hook" => ""} - - checkout_line_item_adjustments(@order).each do |adjustment| - %tr - %td{colspan: "4"}= adjustment.label - %td= adjustment.display_amount.to_html - %td +- checkout_line_item_adjustments(@order).each do |adjustment| + %tr.cart_adjustment + %td{colspan: "3"}= adjustment.label + %td.text-right= adjustment.display_amount.to_html + %td diff --git a/app/views/spree/orders/_form.html.haml b/app/views/spree/orders/_form.html.haml index 435ff03ff6..bba45a53c8 100644 --- a/app/views/spree/orders/_form.html.haml +++ b/app/views/spree/orders/_form.html.haml @@ -1,45 +1,58 @@ = render :partial => 'spree/shared/error_messages', :locals => { :target => @order } -%table#cart-detail{"data-hook" => ""} - %col{halign: "center", valign: "middle", width: "30%"}/ - %col{valign: "middle", width: "25%"}/ - %col{halign: "center", valign: "middle", width: "15%"}/ - %col{halign: "center", valign: "middle", width: "15%"}/ - %col{halign: "center", valign: "middle", width: "10%"}/ - %col{halign: "center", valign: "middle", width: "5%"}/ - %thead - %tr{"data-hook" => "cart_items_headers"} - %th.cart-item-description-header{colspan: "2"}= t(:item) - %th.cart-item-price-header= t(:price) - %th.cart-item-quantity-header= t(:qty) - %th.cart-item-total-header= t(:total) - %th.cart-item-delete-header +.row + .columns.large-12 + %table#cart-detail{"data-hook" => ""} + %col{halign: "left", valign: "middle", width: "60%"}/ + %col{halign: "left", valign: "middle", width: "15%"}/ + %col{halign: "center", valign: "middle", width: "10%"}/ + %col{halign: "center", valign: "middle", width: "10%"}/ + %col{halign: "center", valign: "middle", width: "5%"}/ + %thead + %tr{"data-hook" => "cart_items_headers"} + %th.cart-item-description-header= t(:item) + %th.cart-item-price-header.text-right= t(:price) + %th.text-center.cart-item-quantity-header= t(:qty) + %th.cart-item-total-header.text-right= t(:total) + %th.cart-item-delete-header - %tbody#line_items{"data-hook" => ""} - = order_form.fields_for :line_items do |item_form| - = render :partial => 'line_item', :locals => { :variant => item_form.object.variant, :line_item => item_form.object, :item_form => item_form } + %tbody#line_items{"data-hook" => ""} + = order_form.fields_for :line_items do |item_form| + = render :partial => 'line_item', :locals => { :variant => item_form.object.variant, :line_item => item_form.object, :item_form => item_form } - %tfoot#edit-cart - %tr - %td - Produce Subtotal - \: - %span.order-total.item-total= display_checkout_subtotal(@order) - %td - Admin & Handling - \: - %span.order-total.distribution-total= display_checkout_admin_and_handling_adjustments_total_for(@order) - %td - %td - = button_tag :class => 'secondary radius expand small', :id => 'update-button' do - %i.ofn-i_023-refresh - = t(:update) - %td - %h5.order-total.grand-total= @order.display_total - %td#empty-cart.text-center - %span#clear_cart_link{"data-hook" => ""} - = link_to "Empty cart", empty_cart_path, method: :put, :class => 'not-bold small' - -#= form_tag empty_cart_path, :method => :put do - -#= submit_tag t(:empty_cart), :class => 'button alert expand small' + %tfoot#edit-cart + %tr + %td{colspan:"2"} + %td + = button_tag :class => 'secondary radius expand small', :id => 'update-button' do + %i.ofn-i_023-refresh + = t(:update) + %td + %td#empty-cart.text-center + %span#clear_cart_link{"data-hook" => ""} + = link_to "Empty cart", empty_cart_path, method: :put, :class => 'not-bold small' + -#= form_tag empty_cart_path, :method => :put do + -#= submit_tag t(:empty_cart), :class => 'button alert expand small' - = render "spree/orders/adjustments" unless @order.adjustments.eligible.blank? + / This is the fees row which we want to replace with the pop-over + -# - unless @order.adjustments.eligible.blank? + -# = render "spree/orders/adjustments" + + %tr + %td.text-right{colspan:"3"} Produce subtotal + %td.text-right + %span.order-total.item-total= display_checkout_subtotal(@order) + %td + + %tr + %td.text-right{colspan:"3"} Admin & handling + %td.text-right + %span.order-total.distribution-total= display_checkout_admin_and_handling_adjustments_total_for(@order) + %td + + %tr + %td.text-right{colspan:"3"} + %h5 Total + %td.text-right + %h5.order-total.grand-total= @order.display_total + %td diff --git a/app/views/spree/orders/_line_item.html.haml b/app/views/spree/orders/_line_item.html.haml index 7c11e40bb3..d687290561 100644 --- a/app/views/spree/orders/_line_item.html.haml +++ b/app/views/spree/orders/_line_item.html.haml @@ -1,23 +1,45 @@ %tr.line-item{class: "variant-#{variant.id}"} - %td.cart-item-image{"data-hook" => "cart_item_image"} - - if variant.images.length == 0 - = link_to small_image(variant.product), variant.product - - else - = link_to image_tag(variant.images.first.attachment.url(:small)), variant.product + + / removed image thumbnail on shopping cart & checkout to simplify + / %td.cart-item-image{"data-hook" => "cart_item_image"} + / - if variant.images.length == 0 + / = link_to small_image(variant.product), variant.product + / - else + / = link_to image_tag(variant.images.first.attachment.url(:small)), variant.product %td.cart-item-description{'data-hook' => "cart_item_description"} - %h4= variant.product.name - = variant.options_text + + %div.item-thumb-image{"data-hook" => "cart_item_image"} + - if variant.images.length == 0 + = link_to mini_image(variant.product), variant.product + - else + = link_to image_tag(variant.images.first.attachment.url(:mini)), variant.product + + - if variant.product.name == variant.name_to_display + %h5 + %span= variant.product.name + %span.text-small.text-skinny= " (" + variant.options_text + ")" unless variant.options_text.empty? + - else + %h5 + %span= variant.product.name + %span= "- " + variant.name_to_display + %span.text-small.text-skinny= " (" + variant.options_text + ")" unless variant.options_text.empty? + - if @order.insufficient_stock_lines.include? line_item %span.out-of-stock = variant.in_stock? ? t(:insufficient_stock, :on_hand => variant.on_hand) : t(:out_of_stock) %br/ - %td.cart-item-price{"data-hook" => "cart_item_price"} + %td.text-right.cart-item-price{"data-hook" => "cart_item_price"} = line_item.single_display_amount_with_adjustments.to_html - %td.cart-item-quantity{"data-hook" => "cart_item_quantity"} + -# Now in a template in app/assets/javascripts/templates ! + -# %price-breakdown{"price-breakdown" => "_", variant: "variant", + -# "price-breakdown-append-to-body" => "true", + -# "price-breakdown-placement" => "left", + -# "price-breakdown-animation" => true} + %td.text-center.cart-item-quantity{"data-hook" => "cart_item_quantity"} = item_form.number_field :quantity, :min => 0, :class => "line_item_quantity", :size => 5 - %td.cart-item-total{"data-hook" => "cart_item_total"} + %td.cart-item-total.text-right{"data-hook" => "cart_item_total"} = line_item.display_amount_with_adjustments.to_html unless line_item.quantity.nil? %td.cart-item-delete.text-center{"data-hook" => "cart_item_delete"} diff --git a/app/views/spree/orders/_order_item_description.html.haml b/app/views/spree/orders/_order_item_description.html.haml deleted file mode 100644 index d4531e8976..0000000000 --- a/app/views/spree/orders/_order_item_description.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -%td(data-hook = "order_item_description") - %h4= item.variant.product.name - = "(" + variant_options(item.variant) + ")" unless item.variant.option_values.empty? \ No newline at end of file diff --git a/app/views/spree/orders/show.html.haml b/app/views/spree/orders/show.html.haml index 1f020f8c1f..f9d2830486 100644 --- a/app/views/spree/orders/show.html.haml +++ b/app/views/spree/orders/show.html.haml @@ -1,27 +1,32 @@ -= inject_enterprises += inject_enterprises .darkswarm - content_for :order_cycle_form do %strong.avenir Order ready on - if @order.order_cycle - = pickup_time @order.order_cycle + = @order.order_cycle.pickup_time_for(@order.distributor) - else = @order.distributor.next_collection_at = render partial: "shopping_shared/details" - .row - %fieldset#order_summary{"data-hook" => ""} - %legend{align: "center"}= t(:order) + " #" + @order.number + %fieldset#order_summary{"data-hook" => ""} + .row + .columns.large-12.text-center + %h2 + Order confirmation + = " #" + @order.number - #order{"data-hook" => ""} - - if params.has_key? :checkout_complete - %h1= t(:thank_you_for_your_order) + #order{"data-hook" => ""} + - if params.has_key? :checkout_complete + %h1= t(:thank_you_for_your_order) - = render :partial => 'spree/shared/order_details', :locals => { :order => @order } + = render :partial => 'spree/shared/order_details', :locals => { :order => @order } - = link_to t(:back_to_store), main_app.shop_path, :class => "button" - - unless params.has_key? :checkout_complete - - if try_spree_current_user && respond_to?(:spree_account_path) - = link_to t(:my_account), spree_account_path, :class => "button" + .row + .columns.large-12 + = link_to t(:back_to_store), main_app.shop_path, :class => "button" + - unless params.has_key? :checkout_complete + - if try_spree_current_user && respond_to?(:spree_account_path) + = link_to t(:my_account), spree_account_path, :class => "button" diff --git a/app/views/spree/shared/_order_details.html.haml b/app/views/spree/shared/_order_details.html.haml index a3308ced34..da7bc84ca1 100644 --- a/app/views/spree/shared/_order_details.html.haml +++ b/app/views/spree/shared/_order_details.html.haml @@ -1,95 +1,166 @@ .row - - if order.has_step?("address") - .columns.large-3 - %h6 - = t(:shipping_address) - = link_to "(#{t(:edit)})", checkout_state_path(:address) unless @order.completed? - .address - = order.ship_address - .columns.large-3 - %h6 - = t(:billing_address) - = link_to "(#{t(:edit)})", checkout_state_path(:address) unless @order.completed? - .address - = order.bill_address - - if @order.has_step?("delivery") - .columns.large-2 - %h6 - = t(:shipping_method) - .delivery - = order.shipping_method.name - .columns.large-4 - %h6 - = t(:payment_information) - = link_to "(#{t(:edit)})", checkout_state_path(:payment) unless @order.completed? - .payment-info - = render order.payments.valid + .columns.large-6 + .order-summary.text-small + .right + - if order.paid? + PAID + - else + NOT PAID + %span + Total order + %strong + = order.display_total.to_html + .pad + .text-big + Paying via: + %strong= order.payments.first.andand.payment_method.andand.name.andand.html_safe + %p.text-small.text-skinny.pre-line + %em= order.payments.first.andand.payment_method.andand.description.andand.html_safe -%hr/ + .order-summary.text-small + %strong + Billing address + .pad + %p.text-small + = order.bill_address.firstname + " " + order.bill_address.lastname + %br + = order.bill_address.full_address + %br + = order.bill_address.phone -%table#line-items{"data-hook" => "order_details"} - %col{halign: "center", valign: "middle", width: "15%"}/ - %col{valign: "middle", width: "70%"}/ - %col{halign: "center", valign: "middle", width: "5%"}/ - %col{halign: "center", valign: "middle", width: "5%"}/ - %col{halign: "center", valign: "middle", width: "5%"}/ - %thead{"data-hook" => ""} - %tr{"data-hook" => "order_details_line_items_headers"} - %th{colspan: "2"}= t(:item) - %th.price= t(:price) - %th.qty= t(:qty) - %th.total - %span= t(:total) - %tbody{"data-hook" => ""} - - @order.line_items.each do |item| - %tr{"data-hook" => "order_details_line_item_row"} - %td{"data-hook" => "order_item_image"} - - if item.variant.images.length == 0 - = link_to small_image(item.variant.product), item.variant.product - - else - = link_to image_tag(item.variant.images.first.attachment.url(:small)), item.variant.product - %td{"data-hook" => "order_item_description"} - %h4= item.variant.product.name - = truncated_product_description(item.variant.product) - = "(" + item.variant.options_text + ")" unless item.variant.option_values.empty? - %td.price{"data-hook" => "order_item_price"} - %span= item.single_display_amount_with_adjustments.to_html - %td{"data-hook" => "order_item_qty"}= item.quantity - %td.total{"data-hook" => "order_item_total"} - %span= item.display_amount_with_adjustments.to_html + .columns.large-6 + - if order.shipping_method.andand.require_ship_address + // Delivery option + .order-summary.text-small + %strong= order.shipping_method.name + .pad + .text-big + Delivery on + %strong #{order.order_cycle.pickup_time_for(order.distributor)} + %p.text-small.text-skinny.pre-line + %em= order.shipping_method.description.andand.html_safe || "" + .order-summary.text-small + %strong + Delivery address + .pad + %p.text-small + = order.ship_address.firstname + " " + order.ship_address.lastname + %br + = order.ship_address.full_address + %br + = order.ship_address.phone + - if order.special_instructions.present? + %br + %p.light.small + %strong Your notes: + %br + = order.special_instructions + - else + // Collection option + .order-summary.text-small + %strong= order.shipping_method.name + .pad + .text-big + Ready for collection + %strong #{order.order_cycle.pickup_time_for(order.distributor)} + %p.text-small.text-skinny.pre-line + %em= order.shipping_method.description.andand.html_safe || "" + .order-summary.text-small + %strong + Collection Address + .pad + %p.text-small + = order.ship_address.full_address - %tfoot#order-total{"data-hook" => "order_details_total"} - %tr.total - %td{colspan: "4"} - %b - = t(:order_total) - \: - %td.total - %span#order_total= @order.display_total.to_html - - - if order.price_adjustment_totals.present? - %tfoot#price-adjustments{"data-hook" => "order_details_price_adjustments"} - - @order.price_adjustment_totals.each do |key, total| - %tr.total - %td{colspan: "4"} + - if order.order_cycle.pickup_instructions_for(order.distributor).present? + %br + %p.text-small %strong - = key - \: - %td.total - %span= total + Collection Instructions + %br + #{order.order_cycle.pickup_instructions_for(order.distributor)} - %tfoot#subtotal{"data-hook" => "order_details_subtotal"} - %tr#subtotal-row.total - %td{colspan: "4"} - %b - Produce: - %td.total - %span= display_checkout_subtotal(@order) + - if order.special_instructions.present? + %br + %p.light.small + %strong Your notes: + %br + = order.special_instructions - %tfoot#order-charges{"data-hook" => "order_details_adjustments"} - - checkout_adjustments_for(@order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| - %tr.total - %td{:colspan => "4"} - %strong= adjustment.label + ":" - %td.total - %span= adjustment.display_amount.to_html +%br +.row + .columns.large-12 + %table#line-items{"data-hook" => "order_details"} + %col{valign: "middle"}/ + %col{halign: "center", valign: "middle", width: "5%"}/ + %col{halign: "center", valign: "middle", width: "5%"}/ + %col{halign: "center", valign: "middle", width: "5%"}/ + %thead{"data-hook" => ""} + %tr{"data-hook" => "order_details_line_items_headers"} + %th= t(:item) + %th.price= t(:price) + %th.text-center.qty= t(:qty) + %th.text-right.total + %span= t(:total) + %tbody{"data-hook" => ""} + - order.line_items.each do |item| + %tr{"data-hook" => "order_details_line_item_row"} + %td(data-hook = "order_item_description") + + %div.item-thumb-image{"data-hook" => "order_item_image"} + - if item.variant.images.length == 0 + = link_to mini_image(item.variant.product), item.variant.product + - else + = link_to image_tag(item.variant.images.first.attachment.url(:mini)), item.variant.product + + + - if item.variant.product.name == item.variant.name_to_display + %h5 + %span= item.variant.product.name + %span.text-small.text-skinny= "(" + variant_options(item.variant) + ")" unless item.variant.option_values.empty? + - else + %h5 + %span= item.variant.product.name + %span= "- " + item.variant.name_to_display + %span.text-small.text-skinny= "(" + variant_options(item.variant) + ")" unless item.variant.option_values.empty? + + %td.text-right.price{"data-hook" => "order_item_price"} + %span= item.single_display_amount_with_adjustments.to_html + %td.text-center{"data-hook" => "order_item_qty"}= item.quantity + %td.text-right.total{"data-hook" => "order_item_total"} + %span= item.display_amount_with_adjustments.to_html + + %tfoot#order-total{"data-hook" => "order_details_total"} + %tr.total + %td.text-right{colspan: "3"} + %h5 + Total + %td.text-right.total + %h5#order_total= order.display_total.to_html + + - if order.price_adjustment_totals.present? + %tfoot#price-adjustments{"data-hook" => "order_details_price_adjustments"} + - order.price_adjustment_totals.each do |key, total| + %tr.total + %td.text-right{colspan: "3"} + %strong + = key + %td.text-right.total + %span= total + + %tfoot#subtotal{"data-hook" => "order_details_subtotal"} + %tr#subtotal-row.total + %td.text-right{colspan: "3"} + %strong + Produce + %td.text-right.total + %span= display_checkout_subtotal(order) + + %tfoot#order-charges{"data-hook" => "order_details_adjustments"} + - checkout_adjustments_for(order, exclude: [:line_item]).reject{ |a| a.amount == 0 }.reverse_each do |adjustment| + %tr.total + %td.text-right{:colspan => "3"} + %strong + = adjustment.label + %td.text-right.total + %span= adjustment.display_amount.to_html diff --git a/app/views/spree/users/show.html.haml b/app/views/spree/users/show.html.haml index 49db903a14..878d271e42 100644 --- a/app/views/spree/users/show.html.haml +++ b/app/views/spree/users/show.html.haml @@ -1,33 +1,34 @@ .darkswarm - .row - %h1= accurate_title - .account-summary{"data-hook" => "account_summary"} - %dl#user-info - %dt= t(:email) - %dd - = @user.email - (#{link_to t(:edit), spree.edit_account_path}) - .account-my-orders{"data-hook" => "account_my_orders"} - %h3= t(:my_orders) - - if @orders.present? - %table.order-summary - %thead - %tr - %th.order-number= t(:order_number) - %th.order-date= t(:order_date) - %th.order-status= t(:status) - %th.order-payment-state= t(:payment_state) - %th.order-shipment-state= t(:shipment_state) - %th.order-total= t(:total) - %tbody - - @orders.each do |order| - %tr{class: cycle('even', 'odd')} - %td.order-number= link_to order.number, order_url(order) - %td.order-date= l order.completed_at.to_date - %td.order-status= t(order.state).titleize - %td.order-payment-state= t("payment_states.#{order.payment_state}") if order.payment_state - %td.order-shipment-state= t("shipment_states.#{order.shipment_state}") if order.shipment_state - %td.order-total= money order.total - - else - %p= t(:you_have_no_orders_yet) - %br/ + .row.pad-top + .small-12.columns.pad-top + %h1= accurate_title + .account-summary{"data-hook" => "account_summary"} + %dl#user-info + %dt= t(:email) + %dd + = @user.email + (#{link_to t(:edit), spree.edit_account_path}) + .account-my-orders{"data-hook" => "account_my_orders"} + %h3= t(:my_orders) + - if @orders.present? + %table.order-summary + %thead + %tr + %th.order-number= t(:order_number) + %th.order-date= t(:order_date) + %th.order-status= t(:status) + %th.order-payment-state= t(:payment_state) + %th.order-shipment-state= t(:shipment_state) + %th.order-total= t(:total) + %tbody + - @orders.each do |order| + %tr{class: cycle('even', 'odd')} + %td.order-number= link_to order.number, order_url(order) + %td.order-date= l order.completed_at.to_date + %td.order-status= t(order.state).titleize + %td.order-payment-state= t("payment_states.#{order.payment_state}") if order.payment_state + %td.order-shipment-state= t("shipment_states.#{order.shipment_state}") if order.shipment_state + %td.order-total= money order.total + - else + %p= t(:you_have_no_orders_yet) + %br/ diff --git a/config/application.yml.example b/config/application.yml.example index 6e11a75dca..f953cb4601 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -1,13 +1,15 @@ # Add application configuration variables here, as shown below. # -# Change this, it has serious security implications. +# Change this, it has serious security implications. # Minimum 30 but usually 128 characters. To obtain run 'rake secret', or faster, 'openssl rand -hex 128' -SECRET_TOKEN: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +SECRET_TOKEN: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -TIMEZONE: "Melbourne" +TIMEZONE: Melbourne # Default country for dropdowns etc. -DEFAULT_COUNTRY: "Australia" +DEFAULT_COUNTRY: Australia # Locale for translation. -I18N_LOCALE: "en" +LOCALE: en # Spree zone. -CHECKOUT_ZONE: "Australia" +CHECKOUT_ZONE: Australia +# Find currency codes at http://en.wikipedia.org/wiki/ISO_4217. +CURRENCY: AUD diff --git a/config/initializers/spree.rb b/config/initializers/spree.rb index dca89955b7..a28763f289 100644 --- a/config/initializers/spree.rb +++ b/config/initializers/spree.rb @@ -11,15 +11,16 @@ require 'spree/product_filters' Spree.config do |config| config.shipping_instructions = true - config.checkout_zone = ENV["CHECKOUT_ZONE"] config.address_requires_state = true - # 12 should be Australia. Hardcoded for CI (Jenkins), where countries are not pre-loaded. - if Rails.env.test? or Rails.env.development? - config.default_country_id = 12 - else + # Settings dependent on locale + config.checkout_zone = ENV["CHECKOUT_ZONE"] + config.currency = ENV['CURRENCY'] + if Spree::Country.table_exists? country = Spree::Country.find_by_name(ENV["DEFAULT_COUNTRY"]) config.default_country_id = country.id if country.present? + else + config.default_country_id = 12 # Australia end # -- spree_paypal_express diff --git a/config/routes.rb b/config/routes.rb index e7f9f2bba0..4621ee4a35 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,9 @@ Openfoodnetwork::Application.routes.draw do root :to => 'home#index' + get "/#/login", to: "home#index", as: :spree_login + get "/login", to: redirect("/#/login") get "/map", to: "map#index", as: :map @@ -63,7 +65,10 @@ Openfoodnetwork::Application.routes.draw do resources :enterprise_roles resources :enterprise_fees do - post :bulk_update, :on => :collection, :as => :bulk_update + collection do + get :for_order_cycle + post :bulk_update, :as => :bulk_update + end end resources :enterprise_groups do @@ -123,7 +128,8 @@ Spree::Core::Engine.routes.prepend do match '/admin/reports/bulk_coop' => 'admin/reports#bulk_coop', :as => "bulk_coop_admin_reports", :via => [:get, :post] match '/admin/reports/payments' => 'admin/reports#payments', :as => "payments_admin_reports", :via => [:get, :post] match '/admin/reports/orders_and_fulfillment' => 'admin/reports#orders_and_fulfillment', :as => "orders_and_fulfillment_admin_reports", :via => [:get, :post] - match '/admin/reports/users_and_enterprises' => 'admin/reports#users_and_enterprises', :as => "users_and_enterprises_admin_reports", :via => [:get, :post] + match '/admin/reports/users_and_enterprises' => 'admin/reports#users_and_enterprises', :as => "users_and_enterprises_admin_reports", :via => [:get, :post] + match '/admin/reports/sales_tax' => 'admin/reports#sales_tax', :as => "sales_tax_admin_reports", :via => [:get, :post] match '/admin/products/bulk_edit' => 'admin/products#bulk_edit', :as => "bulk_edit_admin_products" match '/admin/orders/bulk_management' => 'admin/orders#bulk_management', :as => "admin_bulk_order_management" match '/admin/reports/products_and_inventory' => 'admin/reports#products_and_inventory', :as => "products_and_inventory_admin_reports", :via => [:get, :post] @@ -141,7 +147,6 @@ Spree::Core::Engine.routes.prepend do collection do get :managed get :bulk_products - get :distributable get :overridable end delete :soft_delete diff --git a/db/migrate/20141210233407_add_not_null_to_variant_override_relations.rb b/db/migrate/20141210233407_add_not_null_to_variant_override_relations.rb index e10ab41953..b2f50167c9 100644 --- a/db/migrate/20141210233407_add_not_null_to_variant_override_relations.rb +++ b/db/migrate/20141210233407_add_not_null_to_variant_override_relations.rb @@ -1,6 +1,11 @@ class AddNotNullToVariantOverrideRelations < ActiveRecord::Migration - def change + def up change_column :variant_overrides, :hub_id, :integer, null: false change_column :variant_overrides, :variant_id, :integer, null: false end + + def down + change_column :variant_overrides, :hub_id, :integer, null: true + change_column :variant_overrides, :variant_id, :integer, null: true + end end diff --git a/db/migrate/20141219034321_add_permalink_to_enterprises.rb b/db/migrate/20141219034321_add_permalink_to_enterprises.rb index 55d8553bf8..b912180ba7 100644 --- a/db/migrate/20141219034321_add_permalink_to_enterprises.rb +++ b/db/migrate/20141219034321_add_permalink_to_enterprises.rb @@ -2,6 +2,8 @@ class AddPermalinkToEnterprises < ActiveRecord::Migration def up add_column :enterprises, :permalink, :string + Enterprise.reset_column_information + Enterprise.all.each do |enterprise| counter = 1 permalink = enterprise.name.parameterize @@ -11,7 +13,7 @@ class AddPermalinkToEnterprises < ActiveRecord::Migration counter += 1 end - enterprise.update_attributes!(permalink: permalink) + enterprise.update_column :permalink, permalink end change_column :enterprises, :permalink, :string, null: false diff --git a/db/migrate/20150115050935_add_addresses_ref_to_enterprise_groups.rb b/db/migrate/20150115050935_add_addresses_ref_to_enterprise_groups.rb new file mode 100644 index 0000000000..566b35bb88 --- /dev/null +++ b/db/migrate/20150115050935_add_addresses_ref_to_enterprise_groups.rb @@ -0,0 +1,6 @@ +class AddAddressesRefToEnterpriseGroups < ActiveRecord::Migration + def change + add_column :enterprise_groups, :address_id, :integer + add_foreign_key :enterprise_groups, :spree_addresses, column: "address_id" + end +end diff --git a/db/migrate/20150115050936_add_address_instances_to_existing_enterprise_groups.rb b/db/migrate/20150115050936_add_address_instances_to_existing_enterprise_groups.rb new file mode 100644 index 0000000000..23fc038ce8 --- /dev/null +++ b/db/migrate/20150115050936_add_address_instances_to_existing_enterprise_groups.rb @@ -0,0 +1,19 @@ +class AddAddressInstancesToExistingEnterpriseGroups < ActiveRecord::Migration + def change + country = Spree::Country.find_by_name(ENV['DEFAULT_COUNTRY']) + state = country.states.first + EnterpriseGroup.all.each do |g| + next if g.address.present? + address = Spree::Address.new(firstname: 'unused', lastname: 'unused', address1: 'undefined', city: 'undefined', zipcode: 'undefined', state: state, country: country, phone: 'undefined') + g.address = address + # some groups are invalid, because of a missing description + g.save!(validate: false) + end + end + + def self.down + # we can't know which addresses were already there and which weren't + # and we can't remove addresses as long as they are referenced and + # required by the model + end +end diff --git a/db/migrate/20150121030627_add_web_contact_to_enterprise_groups.rb b/db/migrate/20150121030627_add_web_contact_to_enterprise_groups.rb new file mode 100644 index 0000000000..afea2dbeea --- /dev/null +++ b/db/migrate/20150121030627_add_web_contact_to_enterprise_groups.rb @@ -0,0 +1,10 @@ +class AddWebContactToEnterpriseGroups < ActiveRecord::Migration + def change + add_column :enterprise_groups, :email, :string, null: false, default: '' + add_column :enterprise_groups, :website, :string, null: false, default: '' + add_column :enterprise_groups, :facebook, :string, null: false, default: '' + add_column :enterprise_groups, :instagram, :string, null: false, default: '' + add_column :enterprise_groups, :linkedin, :string, null: false, default: '' + add_column :enterprise_groups, :twitter, :string, null: false, default: '' + end +end diff --git a/db/migrate/20150122145607_create_customers.rb b/db/migrate/20150122145607_create_customers.rb new file mode 100644 index 0000000000..12e5a721e8 --- /dev/null +++ b/db/migrate/20150122145607_create_customers.rb @@ -0,0 +1,18 @@ +class CreateCustomers < ActiveRecord::Migration + def change + create_table :customers do |t| + t.string :email, null: false + t.references :enterprise, null: false + t.string :code, null: false + t.references :user + + t.timestamps + end + add_index :customers, [:enterprise_id, :code], unique: true + add_index :customers, :email + add_index :customers, :user_id + + add_foreign_key :customers, :enterprises, column: :enterprise_id + add_foreign_key :customers, :spree_users, column: :user_id + end +end diff --git a/db/migrate/20150202000203_add_owner_to_enterprise_groups.rb b/db/migrate/20150202000203_add_owner_to_enterprise_groups.rb new file mode 100644 index 0000000000..f2e46452df --- /dev/null +++ b/db/migrate/20150202000203_add_owner_to_enterprise_groups.rb @@ -0,0 +1,6 @@ +class AddOwnerToEnterpriseGroups < ActiveRecord::Migration + def change + add_column :enterprise_groups, :owner_id, :integer + add_foreign_key :enterprise_groups, :spree_users, column: "owner_id" + end +end diff --git a/db/migrate/20150216075336_add_temperature_controlled_to_spree_shipping_categories.rb b/db/migrate/20150216075336_add_temperature_controlled_to_spree_shipping_categories.rb new file mode 100644 index 0000000000..89035fc16f --- /dev/null +++ b/db/migrate/20150216075336_add_temperature_controlled_to_spree_shipping_categories.rb @@ -0,0 +1,5 @@ +class AddTemperatureControlledToSpreeShippingCategories < ActiveRecord::Migration + def change + add_column :spree_shipping_categories, :temperature_controlled, :boolean, null: false, default: false + end +end diff --git a/db/migrate/20150219021742_add_owner_index_to_enterprise_groups.rb b/db/migrate/20150219021742_add_owner_index_to_enterprise_groups.rb new file mode 100644 index 0000000000..062e9523e6 --- /dev/null +++ b/db/migrate/20150219021742_add_owner_index_to_enterprise_groups.rb @@ -0,0 +1,5 @@ +class AddOwnerIndexToEnterpriseGroups < ActiveRecord::Migration + def change + add_index :enterprise_groups, :owner_id + end +end diff --git a/db/migrate/20150220035501_add_address_id_index_to_enterprise_groups.rb b/db/migrate/20150220035501_add_address_id_index_to_enterprise_groups.rb new file mode 100644 index 0000000000..32094cbde7 --- /dev/null +++ b/db/migrate/20150220035501_add_address_id_index_to_enterprise_groups.rb @@ -0,0 +1,5 @@ +class AddAddressIdIndexToEnterpriseGroups < ActiveRecord::Migration + def change + add_index :enterprise_groups, :address_id + end +end diff --git a/db/migrate/20150225111538_add_tax_category_to_enterprise_fee.rb b/db/migrate/20150225111538_add_tax_category_to_enterprise_fee.rb new file mode 100644 index 0000000000..05b5410587 --- /dev/null +++ b/db/migrate/20150225111538_add_tax_category_to_enterprise_fee.rb @@ -0,0 +1,7 @@ +class AddTaxCategoryToEnterpriseFee < ActiveRecord::Migration + def change + add_column :enterprise_fees, :tax_category_id, :integer + add_foreign_key :enterprise_fees, :spree_tax_categories, column: :tax_category_id + add_index :enterprise_fees, :tax_category_id + end +end diff --git a/db/migrate/20150225232938_add_included_tax_to_adjustments.rb b/db/migrate/20150225232938_add_included_tax_to_adjustments.rb new file mode 100644 index 0000000000..f3815dec2a --- /dev/null +++ b/db/migrate/20150225232938_add_included_tax_to_adjustments.rb @@ -0,0 +1,5 @@ +class AddIncludedTaxToAdjustments < ActiveRecord::Migration + def change + add_column :spree_adjustments, :included_tax, :decimal, precision: 10, scale: 2, null: false, default: 0 + end +end diff --git a/db/migrate/20150410043302_create_delayed_jobs.rb b/db/migrate/20150410043302_create_delayed_jobs.rb new file mode 100644 index 0000000000..f7de70bdc5 --- /dev/null +++ b/db/migrate/20150410043302_create_delayed_jobs.rb @@ -0,0 +1,22 @@ +class CreateDelayedJobs < ActiveRecord::Migration + def self.up + create_table :delayed_jobs, :force => true do |table| + table.integer :priority, :default => 0, :null => false # Allows some jobs to jump to the front of the queue + table.integer :attempts, :default => 0, :null => false # Provides for retries, but still fail eventually. + table.text :handler, :null => false # YAML-encoded string of the object that will do work + table.text :last_error # reason for last failure (See Note below) + table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. + table.datetime :locked_at # Set when a client is working on this object + table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) + table.string :locked_by # Who is working on this object (if locked) + table.string :queue # The name of the queue this job is in + table.timestamps + end + + add_index :delayed_jobs, [:priority, :run_at], :name => 'delayed_jobs_priority' + end + + def self.down + drop_table :delayed_jobs + end +end diff --git a/db/schema.rb b/db/schema.rb index 2d8f28fbdd..f30408c2ac 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 => 20141219034321) do +ActiveRecord::Schema.define(:version => 20150410043302) do create_table "adjustment_metadata", :force => true do |t| t.integer "adjustment_id" @@ -155,6 +155,35 @@ ActiveRecord::Schema.define(:version => 20141219034321) do add_index "coordinator_fees", ["enterprise_fee_id"], :name => "index_coordinator_fees_on_enterprise_fee_id" add_index "coordinator_fees", ["order_cycle_id"], :name => "index_coordinator_fees_on_order_cycle_id" + create_table "customers", :force => true do |t| + t.string "email", :null => false + t.integer "enterprise_id", :null => false + t.string "code", :null => false + t.integer "user_id" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "customers", ["email"], :name => "index_customers_on_email" + add_index "customers", ["enterprise_id", "code"], :name => "index_customers_on_enterprise_id_and_code", :unique => true + add_index "customers", ["user_id"], :name => "index_customers_on_user_id" + + create_table "delayed_jobs", :force => true do |t| + t.integer "priority", :default => 0, :null => false + t.integer "attempts", :default => 0, :null => false + t.text "handler", :null => false + t.text "last_error" + t.datetime "run_at" + t.datetime "locked_at" + t.datetime "failed_at" + t.string "locked_by" + t.string "queue" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority" + create_table "distributors_payment_methods", :id => false, :force => true do |t| t.integer "distributor_id" t.integer "payment_method_id" @@ -177,11 +206,13 @@ ActiveRecord::Schema.define(:version => 20141219034321) do t.integer "enterprise_id" t.string "fee_type" t.string "name" - 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.integer "tax_category_id" end add_index "enterprise_fees", ["enterprise_id"], :name => "index_enterprise_fees_on_enterprise_id" + add_index "enterprise_fees", ["tax_category_id"], :name => "index_enterprise_fees_on_tax_category_id" create_table "enterprise_groups", :force => true do |t| t.string "name" @@ -197,8 +228,19 @@ ActiveRecord::Schema.define(:version => 20141219034321) do t.string "logo_content_type" t.integer "logo_file_size" t.datetime "logo_updated_at" + t.integer "address_id" + t.string "email", :default => "", :null => false + t.string "website", :default => "", :null => false + t.string "facebook", :default => "", :null => false + t.string "instagram", :default => "", :null => false + t.string "linkedin", :default => "", :null => false + t.string "twitter", :default => "", :null => false + t.integer "owner_id" end + add_index "enterprise_groups", ["address_id"], :name => "index_enterprise_groups_on_address_id" + add_index "enterprise_groups", ["owner_id"], :name => "index_enterprise_groups_on_owner_id" + create_table "enterprise_groups_enterprises", :id => false, :force => true do |t| t.integer "enterprise_group_id" t.integer "enterprise_id" @@ -272,6 +314,7 @@ ActiveRecord::Schema.define(:version => 20141219034321) do t.datetime "shop_trial_start_date" t.boolean "producer_profile_only", :default => false t.string "permalink", :null => false + t.boolean "charges_sales_tax", :default => false, :null => false end add_index "enterprises", ["address_id"], :name => "index_enterprises_on_address_id" @@ -403,6 +446,7 @@ ActiveRecord::Schema.define(:version => 20141219034321) do t.string "originator_type" t.boolean "eligible", :default => true t.string "adjustable_type" + t.decimal "included_tax", :precision => 10, :scale => 2, :default => 0.0, :null => false end add_index "spree_adjustments", ["adjustable_id"], :name => "index_adjustments_on_order_id" @@ -574,9 +618,9 @@ ActiveRecord::Schema.define(:version => 20141219034321) do t.string "email" t.text "special_instructions" t.integer "distributor_id" - t.integer "order_cycle_id" t.string "currency" t.string "last_ip_address" + t.integer "order_cycle_id" t.integer "cart_id" end @@ -837,8 +881,9 @@ ActiveRecord::Schema.define(:version => 20141219034321) do create_table "spree_shipping_categories", :force => true do |t| t.string "name" - 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.boolean "temperature_controlled", :default => false, :null => false end create_table "spree_shipping_methods", :force => true do |t| @@ -1070,6 +1115,9 @@ ActiveRecord::Schema.define(:version => 20141219034321) do add_foreign_key "coordinator_fees", "enterprise_fees", name: "coordinator_fees_enterprise_fee_id_fk" add_foreign_key "coordinator_fees", "order_cycles", name: "coordinator_fees_order_cycle_id_fk" + add_foreign_key "customers", "enterprises", name: "customers_enterprise_id_fk" + add_foreign_key "customers", "spree_users", name: "customers_user_id_fk", column: "user_id" + add_foreign_key "distributors_payment_methods", "enterprises", name: "distributors_payment_methods_distributor_id_fk", column: "distributor_id" add_foreign_key "distributors_payment_methods", "spree_payment_methods", name: "distributors_payment_methods_payment_method_id_fk", column: "payment_method_id" @@ -1077,6 +1125,10 @@ ActiveRecord::Schema.define(:version => 20141219034321) do add_foreign_key "distributors_shipping_methods", "spree_shipping_methods", name: "distributors_shipping_methods_shipping_method_id_fk", column: "shipping_method_id" add_foreign_key "enterprise_fees", "enterprises", name: "enterprise_fees_enterprise_id_fk" + add_foreign_key "enterprise_fees", "spree_tax_categories", name: "enterprise_fees_tax_category_id_fk", column: "tax_category_id" + + add_foreign_key "enterprise_groups", "spree_addresses", name: "enterprise_groups_address_id_fk", column: "address_id" + add_foreign_key "enterprise_groups", "spree_users", name: "enterprise_groups_owner_id_fk", column: "owner_id" add_foreign_key "enterprise_groups_enterprises", "enterprise_groups", name: "enterprise_groups_enterprises_enterprise_group_id_fk" add_foreign_key "enterprise_groups_enterprises", "enterprises", name: "enterprise_groups_enterprises_enterprise_id_fk" diff --git a/lib/open_food_network/enterprise_fee_applicator.rb b/lib/open_food_network/enterprise_fee_applicator.rb index 45905bfaca..070cb4a91c 100644 --- a/lib/open_food_network/enterprise_fee_applicator.rb +++ b/lib/open_food_network/enterprise_fee_applicator.rb @@ -2,12 +2,18 @@ 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 + + a.set_absolute_included_tax! adjustment_tax(line_item.order, a) 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 + + a.set_absolute_included_tax! adjustment_tax(order, a) end @@ -24,6 +30,48 @@ module OpenFoodNetwork def base_adjustment_label "#{enterprise_fee.fee_type} fee by #{role} #{enterprise_fee.enterprise.name}" end + + def adjustment_tax(order, adjustment) + tax_rates = enterprise_fee.tax_category ? enterprise_fee.tax_category.tax_rates.match(order) : [] + + tax_rates.sum do |rate| + compute_tax rate, adjustment.amount + end + end + + # Apply a TaxRate to a particular amount. TaxRates normally compute against + # LineItems or Orders, so we mock out a line item here to fit the interface + # that our calculator (usually DefaultTax) expects. + def compute_tax(tax_rate, amount) + product = OpenStruct.new tax_category: tax_rate.tax_category + line_item = Spree::LineItem.new quantity: 1 + line_item.define_singleton_method(:product) { product } + line_item.define_singleton_method(:price) { amount } + + # The enterprise fee adjustments for which we're calculating tax are always inclusive of + # tax. However, there's nothing to stop an admin from setting one up with a tax rate + # that's marked as not inclusive of tax, and that would result in the DefaultTax + # calculator generating a slightly incorrect value. Therefore, we treat the tax + # rate as inclusive of tax for the calculations below, regardless of its original + # setting. + with_tax_included_in_price(tax_rate) do + tax_rate.calculator.compute line_item + end + end + + def with_tax_included_in_price(tax_rate) + old_included_in_price = tax_rate.included_in_price + + tax_rate.included_in_price = true + tax_rate.calculator.calculable.included_in_price = true + + result = yield + + tax_rate.included_in_price = old_included_in_price + tax_rate.calculator.calculable.included_in_price = old_included_in_price + + result + end end end diff --git a/lib/open_food_network/locking.rb b/lib/open_food_network/locking.rb new file mode 100644 index 0000000000..30dec63b0b --- /dev/null +++ b/lib/open_food_network/locking.rb @@ -0,0 +1,13 @@ +module OpenFoodNetwork::Locking + # http://rhnh.net/2010/06/30/acts-as-list-will-break-in-production + def with_isolation_level_serializable + self.transaction do + self.connection.execute "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE" + yield + end + end +end + +class ActiveRecord::Base + extend OpenFoodNetwork::Locking +end diff --git a/lib/open_food_network/order_cycle_form_applicator.rb b/lib/open_food_network/order_cycle_form_applicator.rb index 6556223dac..69ba284cd8 100644 --- a/lib/open_food_network/order_cycle_form_applicator.rb +++ b/lib/open_food_network/order_cycle_form_applicator.rb @@ -6,9 +6,9 @@ module OpenFoodNetwork # as much as possible (if not all) of its logic into Angular. class OrderCycleFormApplicator # The applicator will only touch exchanges where a permitted enterprise is the participant - def initialize(order_cycle, permitted_enterprises) + def initialize(order_cycle, spree_current_user) @order_cycle = order_cycle - @permitted_enterprises = permitted_enterprises + @spree_current_user = spree_current_user end def go! @@ -16,7 +16,7 @@ module OpenFoodNetwork @order_cycle.incoming_exchanges ||= [] @order_cycle.incoming_exchanges.each do |exchange| - variant_ids = exchange_variant_ids(exchange) + variant_ids = incoming_exchange_variant_ids(exchange) enterprise_fee_ids = exchange[:enterprise_fee_ids] if exchange_exists?(exchange[:enterprise_id], @order_cycle.coordinator_id, true) @@ -30,7 +30,7 @@ module OpenFoodNetwork @order_cycle.outgoing_exchanges ||= [] @order_cycle.outgoing_exchanges.each do |exchange| - variant_ids = exchange_variant_ids(exchange) + variant_ids = outgoing_exchange_variant_ids(exchange) enterprise_fee_ids = exchange[:enterprise_fee_ids] if exchange_exists?(@order_cycle.coordinator_id, exchange[:enterprise_id], false) @@ -60,7 +60,7 @@ module OpenFoodNetwork attrs = attrs.reverse_merge(:sender_id => sender_id, :receiver_id => receiver_id, :incoming => incoming) exchange = @order_cycle.exchanges.build attrs - if permission_for exchange + if manages_coordinator? exchange.save! @touched_exchanges << exchange end @@ -69,6 +69,12 @@ module OpenFoodNetwork def update_exchange(sender_id, receiver_id, incoming, attrs={}) exchange = @order_cycle.exchanges.where(:sender_id => sender_id, :receiver_id => receiver_id, :incoming => incoming).first + unless manages_coordinator? || manager_for(exchange) + attrs.delete :enterprise_fee_ids + attrs.delete :pickup_time + attrs.delete :pickup_instructions + end + if permission_for exchange exchange.update_attributes!(attrs) @touched_exchanges << exchange @@ -76,7 +82,9 @@ module OpenFoodNetwork end def destroy_untouched_exchanges - with_permission(untouched_exchanges).each(&:destroy) + if manages_coordinator? + untouched_exchanges.each(&:destroy) + end end def untouched_exchanges @@ -84,17 +92,80 @@ module OpenFoodNetwork @order_cycle.exchanges.reject { |ex| touched_exchange_ids.include? ex.id } end - def with_permission(exchanges) - exchanges.select { |ex| permission_for(ex) } + def manager_for(exchange) + Enterprise.managed_by(@spree_current_user).include? exchange.participant end def permission_for(exchange) - @permitted_enterprises.include? exchange.participant + permitted_enterprises.include? exchange.participant end + def permitted_enterprises + return @permitted_enterprises unless @permitted_enterprises.nil? + @permitted_enterprises = OpenFoodNetwork::OrderCyclePermissions. + new(@spree_current_user, @order_cycle).visible_enterprises + end - def exchange_variant_ids(exchange) - exchange[:variants].select { |k, v| v }.keys.map { |k| k.to_i } + def manages_coordinator? + return @manages_coordinator unless @manages_coordinator.nil? + @manages_coordinator = Enterprise.managed_by(@spree_current_user).include? @order_cycle.coordinator + end + + def editable_variant_ids_for_incoming_exchange_between(sender, receiver) + OpenFoodNetwork::OrderCyclePermissions.new(@spree_current_user, @order_cycle). + editable_variants_for_incoming_exchanges_from(sender).pluck(:id) + end + + def editable_variant_ids_for_outgoing_exchange_between(sender, receiver) + OpenFoodNetwork::OrderCyclePermissions.new(@spree_current_user, @order_cycle). + editable_variants_for_outgoing_exchanges_to(receiver).pluck(:id) + end + + def find_incoming_exchange(attrs) + @order_cycle.exchanges. + where(:sender_id => attrs[:enterprise_id], :receiver_id => @order_cycle.coordinator_id, :incoming => true).first + end + + def find_outgoing_exchange(attrs) + @order_cycle.exchanges. + where(:sender_id => @order_cycle.coordinator_id, :receiver_id => attrs[:enterprise_id], :incoming => false).first + end + + def persisted_variants_hash(exchange) + exchange ||= OpenStruct.new(variants: []) + Hash[ exchange.variants.map{ |v| [v.id, true] } ] + end + + def incoming_exchange_variant_ids(attrs) + exchange = find_incoming_exchange(attrs) + variants = persisted_variants_hash(exchange) + + sender = exchange.andand.sender || Enterprise.find(attrs[:enterprise_id]) + receiver = @order_cycle.coordinator + permitted = editable_variant_ids_for_incoming_exchange_between(sender, receiver) + + # Only change visibility for variants I have permission to edit + attrs[:variants].each do |variant_id, value| + variants[variant_id.to_i] = value if permitted.include?(variant_id.to_i) + end + + variants.select { |k, v| v }.keys.map { |k| k.to_i }.sort + end + + def outgoing_exchange_variant_ids(attrs) + exchange = find_outgoing_exchange(attrs) + variants = persisted_variants_hash(exchange) + + sender = @order_cycle.coordinator + receiver = exchange.andand.receiver || Enterprise.find(attrs[:enterprise_id]) + permitted = editable_variant_ids_for_outgoing_exchange_between(sender, receiver) + + # Only change visibility for variants I have permission to edit + attrs[:variants].each do |variant_id, value| + variants[variant_id.to_i] = value if permitted.include?(variant_id.to_i) + end + + variants.select { |k, v| v }.keys.map { |k| k.to_i }.sort end end end diff --git a/lib/open_food_network/order_cycle_management_report.rb b/lib/open_food_network/order_cycle_management_report.rb index 06a90bfe25..b6e65f0a61 100644 --- a/lib/open_food_network/order_cycle_management_report.rb +++ b/lib/open_food_network/order_cycle_management_report.rb @@ -1,3 +1,5 @@ +require 'open_food_network/user_balance_calculator' + module OpenFoodNetwork class OrderCycleManagementReport attr_reader :params @@ -7,23 +9,19 @@ module OpenFoodNetwork end def header - ["First Name", "Last Name", "Email", "Phone", "Hub", "Shipping Method", "Payment Method", "Amount"] + if is_payment_methods? + ["First Name", "Last Name", "Hub", "Hub Code", "Email", "Phone", "Shipping Method", "Payment Method", "Amount", "Balance"] + else + ["First Name", "Last Name", "Hub", "Hub Code", "Delivery Address", "Delivery Postcode", "Phone", "Shipping Method", "Payment Method", "Amount", "Balance", "Temp Controlled Items?", "Special Instructions"] + end end def table - orders.map do |order| - ba = order.billing_address - da = order.distributor.andand.address - [ba.firstname, - ba.lastname, - order.email, - ba.phone, - order.distributor.andand.name, - order.shipping_method.andand.name, - order.payments.first.andand.payment_method.andand.name, - order.payments.first.amount - ] - end + if is_payment_methods? + orders.map { |o| payment_method_row o } + else + orders.map { |o| delivery_row o } + end end def orders @@ -34,6 +32,44 @@ module OpenFoodNetwork filter_to_order_cycle filter_to_payment_method filter_to_shipping_method orders end + + private + + def payment_method_row(order) + ba = order.billing_address + da = order.distributor.andand.address + [ba.firstname, + ba.lastname, + order.distributor.andand.name, + customer_code(order.email), + order.email, + ba.phone, + order.shipping_method.andand.name, + order.payments.first.andand.payment_method.andand.name, + order.payments.first.amount, + OpenFoodNetwork::UserBalanceCalculator.new(order.user, order.distributor).balance + ] + end + + def delivery_row(order) + ba = order.billing_address + da = order.distributor.andand.address + [ba.firstname, + ba.lastname, + order.distributor.andand.name, + customer_code(order.email), + "#{ba.address1} #{ba.address2} #{ba.city}", + ba.zipcode, + ba.phone, + order.shipping_method.andand.name, + order.payments.first.andand.payment_method.andand.name, + order.payments.first.amount, + OpenFoodNetwork::UserBalanceCalculator.new(order.user, order.distributor).balance, + has_temperature_controlled_items?(order), + order.special_instructions + ] + end + def filter_to_payment_method(orders) if params[:payment_method_name].present? orders.with_payment_method_name(params[:payment_method_name]) @@ -57,5 +93,18 @@ module OpenFoodNetwork orders end end + + def has_temperature_controlled_items?(order) + order.line_items.any? { |line_item| line_item.product.shipping_category.andand.temperature_controlled } + end + + def is_payment_methods? + params[:report_type] == "payment_methods" + end + + def customer_code(email) + customer = Customer.where(email: email).first + customer.nil? ? "" : customer.code + end end end diff --git a/lib/open_food_network/order_cycle_permissions.rb b/lib/open_food_network/order_cycle_permissions.rb new file mode 100644 index 0000000000..2af60c5696 --- /dev/null +++ b/lib/open_food_network/order_cycle_permissions.rb @@ -0,0 +1,251 @@ +module OpenFoodNetwork + # Class which is used for determining the permissions around a single order cycle and user + # both of which are set at initialization + class OrderCyclePermissions < Permissions + def initialize(user, order_cycle) + super(user) + @order_cycle = order_cycle + @coordinator = order_cycle.andand.coordinator + end + + # List of any enterprises whose exchanges I should be able to see in order_cycle + # NOTE: the enterprises a given user can see actually in the OC interface depend on the relationships + # of their enterprises to the coordinator of the order cycle, rather than on the order cycle itself + def visible_enterprises + return Enterprise.where("1=0") unless @coordinator.present? + if managed_enterprises.include? @coordinator + coordinator_permitted = [@coordinator] + all_active = [] + + if @coordinator.sells == "any" + # If the coordinator sells any, relationships come into play + granting(:add_to_order_cycle, to: [@coordinator]).pluck(:id).each do |enterprise_id| + coordinator_permitted << enterprise_id + end + + # As a safety net, we should load all of the enterprises invloved in existing exchanges in this order cycle + all_active = @order_cycle.suppliers.pluck(:id) | @order_cycle.distributors.pluck(:id) + end + + Enterprise.where(id: coordinator_permitted | all_active) + else + # Any enterprises that I manage directly, which have granted P-OC to the coordinator + managed_permitted = granting(:add_to_order_cycle, to: [@coordinator], scope: managed_participating_enterprises ).pluck(:id) + + # Any hubs in this OC that have been granted P-OC by producers I manage in this OC + hubs_permitted = granted(:add_to_order_cycle, by: managed_participating_producers, scope: @order_cycle.distributors).pluck(:id) + + # Any hubs in this OC that have granted P-OC to producers I manage in this OC + hubs_permitting = granting(:add_to_order_cycle, to: managed_participating_producers, scope: @order_cycle.distributors).pluck(:id) + + # Any producers in this OC that have been granted P-OC by hubs I manage in this OC + producers_permitted = granted(:add_to_order_cycle, by: managed_participating_hubs, scope: @order_cycle.suppliers).pluck(:id) + + # Any producers in this OC that have granted P-OC to hubs I manage in this OC + producers_permitting = granting(:add_to_order_cycle, to: managed_participating_hubs, scope: @order_cycle.suppliers).pluck(:id) + + managed_active = [] + hubs_active = [] + producers_active = [] + if @order_cycle + # TODO: Remove this when all P-OC are sorted out + # Any enterprises that I manage that are already in the order_cycle + managed_active = managed_participating_enterprises.pluck(:id) + + # TODO: Remove this when all P-OC are sorted out + # Any hubs that currently have outgoing exchanges distributing variants of producers I manage + variants = Spree::Variant.joins(:product).where('spree_products.supplier_id IN (?)', managed_enterprises.is_primary_producer) + active_exchanges = @order_cycle.exchanges.outgoing.with_any_variant(variants) + hubs_active = active_exchanges.map(&:receiver_id) + + # TODO: Remove this when all P-OC are sorted out + # Any producers of variants that hubs I manage are currently distributing in this OC + variants = Spree::Variant.joins(:exchanges).where("exchanges.receiver_id IN (?) AND exchanges.order_cycle_id = (?) AND exchanges.incoming = 'f'", managed_participating_hubs, @order_cycle).pluck(:id).uniq + products = Spree::Product.joins(:variants_including_master).where("spree_variants.id IN (?)", variants).pluck(:id).uniq + producers_active = Enterprise.joins(:supplied_products).where("spree_products.id IN (?)", products).pluck(:id).uniq + end + + ids = managed_permitted | hubs_permitted | hubs_permitting | producers_permitted | producers_permitting | managed_active | hubs_active | producers_active + + Enterprise.where(id: ids.sort ) + end + end + + # Find the exchanges of an order cycle that an admin can manage + def visible_exchanges + ids = order_cycle_exchange_ids_involving_my_enterprises | + order_cycle_exchange_ids_distributing_my_variants | + order_cycle_exchange_ids_with_distributable_variants + + Exchange.where(id: ids, order_cycle_id: @order_cycle) + end + + # Find the variants that a user can POTENTIALLY see within incoming exchanges + def visible_variants_for_incoming_exchanges_from(producer) + return Spree::Variant.where("1=0") unless @order_cycle + + if user_manages_coordinator_or(producer) + # All variants belonging to the producer + Spree::Variant.joins(:product).where('spree_products.supplier_id = (?)', producer) + else + # All variants of the producer if it has granted P-OC to any of my managed hubs that are in this order cycle + permitted = EnterpriseRelationship.permitting(managed_participating_hubs). + permitted_by(producer).with_permission(:add_to_order_cycle).present? + if permitted + Spree::Variant.joins(:product).where('spree_products.supplier_id = (?)', producer) + else + Spree::Variant.where("1=0") + end + end + end + + # Find the variants that a user can edit within incoming exchanges + def editable_variants_for_incoming_exchanges_from(producer) + return Spree::Variant.where("1=0") unless @order_cycle + + if user_manages_coordinator_or(producer) + # All variants belonging to the producer + Spree::Variant.joins(:product).where('spree_products.supplier_id = (?)', producer) + else + Spree::Variant.where("1=0") + end + end + + # Find the variants that a user is permitted see within outgoing exchanges + # Note that this does not determine whether they actually appear in outgoing exchanges + # as this requires first that the variant is included in an incoming exchange + def visible_variants_for_outgoing_exchanges_to(hub) + return Spree::Variant.where("1=0") unless @order_cycle + + if user_manages_coordinator_or(hub) + # Any variants produced by the coordinator, for outgoing exchanges with itself + coordinator_variants = [] + if hub == @coordinator + coordinator_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id = (?)', @coordinator) + end + + # Any variants of any producers that have granted the hub P-OC + producers = granting(:add_to_order_cycle, to: [hub], scope: Enterprise.is_primary_producer) + permitted_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id IN (?)', producers) + + # PLUS any variants that are already in an outgoing exchange of this hub, so things don't break + # TODO: Remove this when all P-OC are sorted out + active_variants = [] + @order_cycle.exchanges.outgoing.where(receiver_id: hub).limit(1).each do |exchange| + active_variants = exchange.variants + end + + Spree::Variant.where(id: coordinator_variants | permitted_variants | active_variants) + else + # Any variants produced by MY PRODUCERS that are in this order cycle, where my producer has granted P-OC to the hub + producers = granting(:add_to_order_cycle, to: [hub], scope: managed_participating_producers) + permitted_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id IN (?)', producers) + + # PLUS any of my incoming producers' variants that are already in an outgoing exchange of this hub, so things don't break + # TODO: Remove this when all P-OC are sorted out + active_variants = Spree::Variant.joins(:exchanges, :product). + where("exchanges.receiver_id = (?) AND spree_products.supplier_id IN (?) AND incoming = 'f'", hub, managed_enterprises.is_primary_producer) + + Spree::Variant.where(id: permitted_variants | active_variants) + end + end + + # Find the variants that a user is permitted edit within outgoing exchanges + def editable_variants_for_outgoing_exchanges_to(hub) + return Spree::Variant.where("1=0") unless @order_cycle + + if user_manages_coordinator_or(hub) + # Any variants produced by the coordinator, for outgoing exchanges with itself + coordinator_variants = [] + if hub == @coordinator + coordinator_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id = (?)', @coordinator) + end + + # Any variants of any producers that have granted the hub P-OC + producers = granting(:add_to_order_cycle, to: [hub], scope: Enterprise.is_primary_producer) + permitted_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id IN (?)', producers) + + # PLUS any variants that are already in an outgoing exchange of this hub, so things don't break + # TODO: Remove this when all P-OC are sorted out + active_variants = [] + @order_cycle.exchanges.outgoing.where(receiver_id: hub).limit(1).each do |exchange| + active_variants = exchange.variants + end + + Spree::Variant.where(id: coordinator_variants | permitted_variants | active_variants) + else + # Any of my managed producers in this order cycle granted P-OC by the hub + granted_producers = granted(:add_to_order_cycle, by: [hub], scope: managed_participating_producers) + + # Any variants produced by MY PRODUCERS that are in this order cycle, where my producer has granted P-OC to the hub + granting_producers = granting(:add_to_order_cycle, to: [hub], scope: granted_producers) + permitted_variants = Spree::Variant.joins(:product).where('spree_products.supplier_id IN (?)', granting_producers) + + Spree::Variant.where(id: permitted_variants) + end + end + + + private + + def user_manages_coordinator_or(enterprise) + managed_enterprises.pluck(:id).include?(@coordinator.id) || managed_enterprises.pluck(:id).include?(enterprise.id) + end + + def managed_participating_enterprises + return @managed_participating_enterprises unless @managed_participating_enterprises.nil? + @managed_participating_enterprises = managed_enterprises.where(id: @order_cycle.suppliers | @order_cycle.distributors) + end + + def managed_participating_hubs + return @managed_participating_hubs unless @managed_participating_hubs.nil? + @managed_participating_hubs = managed_participating_enterprises.is_hub + end + + def managed_participating_producers + return @managed_participating_producers unless @managed_participating_producers.nil? + @managed_participating_producers = managed_participating_enterprises.is_primary_producer + end + + def order_cycle_exchange_ids_involving_my_enterprises + # Any exchanges that my managed enterprises are involved in directly + @order_cycle.exchanges.involving(managed_enterprises).pluck :id + end + + def order_cycle_exchange_ids_with_distributable_variants + # Find my managed hubs in this order cycle + hubs = managed_participating_hubs + # Any incoming exchange where the producer has granted P-OC to one or more of those hubs + producers = granting(:add_to_order_cycle, to: hubs, scope: Enterprise.is_primary_producer).pluck :id + permitted_exchanges = @order_cycle.exchanges.incoming.where(sender_id: producers).pluck :id + + # TODO: remove active_exchanges when we think it is safe to do so + # active_exchanges is for backward compatability, before we restricted variants in each + # outgoing exchange to those where the producer had granted P-OC to the distributor + # For any of my managed hubs in this OC, any incoming exchanges supplying variants in my outgoing exchanges + variants = Spree::Variant.joins(:exchanges).where("exchanges.receiver_id IN (?) AND exchanges.order_cycle_id = (?) AND exchanges.incoming = 'f'", hubs, @order_cycle).pluck(:id).uniq + products = Spree::Product.joins(:variants_including_master).where("spree_variants.id IN (?)", variants).pluck(:id).uniq + producers = Enterprise.joins(:supplied_products).where("spree_products.id IN (?)", products).pluck(:id).uniq + active_exchanges = @order_cycle.exchanges.incoming.where(sender_id: producers).pluck :id + + permitted_exchanges | active_exchanges + end + + def order_cycle_exchange_ids_distributing_my_variants + # Find my producers in this order cycle + producers = managed_participating_producers.pluck :id + # Any outgoing exchange where the distributor has been granted P-OC by one or more of those producers + hubs = granted(:add_to_order_cycle, by: producers, scope: Enterprise.is_hub) + permitted_exchanges = @order_cycle.exchanges.outgoing.where(receiver_id: hubs).pluck :id + + # TODO: remove active_exchanges when we think it is safe to do so + # active_exchanges is for backward compatability, before we restricted variants in each + # outgoing exchange to those where the producer had granted P-OC to the distributor + # For any of my managed producers, any outgoing exchanges with their variants + variants = Spree::Variant.joins(:product).where('spree_products.supplier_id IN (?)', producers) + active_exchanges = @order_cycle.exchanges.outgoing.with_any_variant(variants).pluck :id + + permitted_exchanges | active_exchanges + end + end +end diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index b917d1fc7d..4edb3d5f39 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -20,6 +20,10 @@ module OpenFoodNetwork managed_and_related_enterprises_with :edit_profile end + def variant_override_hubs + managed_and_related_enterprises_with(:add_to_order_cycle).is_hub + end + def variant_override_producers producer_ids = variant_override_enterprises_per_hub.values.flatten.uniq Enterprise.where(id: producer_ids) @@ -52,13 +56,6 @@ module OpenFoodNetwork permissions end - - # Find the exchanges of an order cycle that an admin can manage - def order_cycle_exchanges(order_cycle) - enterprises = managed_and_related_enterprises_with :add_to_order_cycle - order_cycle.exchanges.to_enterprises(enterprises).from_enterprises(enterprises) - end - def managed_products managed_enterprise_products_ids = managed_enterprise_products.pluck :id permitted_enterprise_products_ids = related_enterprise_products.pluck :id @@ -84,7 +81,8 @@ module OpenFoodNetwork end def managed_enterprises - Enterprise.managed_by(@user) + return @managed_enterprises unless @managed_enterprises.nil? + @managed_enterprises = Enterprise.managed_by(@user) end def related_enterprises_with(permission) @@ -96,6 +94,24 @@ module OpenFoodNetwork Enterprise.where('id IN (?)', parent_ids) end + def granting(permission, options={}) + parent_ids = EnterpriseRelationship. + permitting(options[:to] || managed_enterprises). + with_permission(permission). + pluck(:parent_id) + + (options[:scope] || Enterprise).where('enterprises.id IN (?)', parent_ids) + end + + def granted(permission, options={}) + child_ids = EnterpriseRelationship. + permitted_by(options[:by] || managed_enterprises). + with_permission(permission). + pluck(:child_id) + + (options[:scope] || Enterprise).where('enterprises.id IN (?)', child_ids) + end + def managed_enterprise_products Spree::Product.managed_by(@user) end diff --git a/lib/open_food_network/sales_tax_report.rb b/lib/open_food_network/sales_tax_report.rb new file mode 100644 index 0000000000..46e2cd234e --- /dev/null +++ b/lib/open_food_network/sales_tax_report.rb @@ -0,0 +1,68 @@ +module OpenFoodNetwork + class SalesTaxReport + include Spree::ReportsHelper + + def initialize orders + @orders = orders + end + + def header + ["Order number", "Date", "Items", "Items total (#{currency_symbol})", "Taxable Items Total (#{currency_symbol})", + "Sales Tax (#{currency_symbol})", "Delivery Charge (#{currency_symbol})", "Tax on Delivery (#{currency_symbol})", "Tax on Fees (#{currency_symbol})", + "Total Tax (#{currency_symbol})", "Customer", "Distributor"] + end + + def table + @orders.map do |order| + totals = totals_of order.line_items + shipping_cost = shipping_cost_for order + + [order.number, order.created_at, totals[:items], totals[:items_total], + totals[:taxable_total], totals[:sales_tax], shipping_cost, order.shipping_tax, order.enterprise_fee_tax, order.total_tax, + order.bill_address.full_name, order.distributor.andand.name] + end + end + + + private + + def totals_of(line_items) + totals = {items: 0, items_total: 0.0, taxable_total: 0.0, sales_tax: 0.0} + + line_items.each do |line_item| + totals[:items] += line_item.quantity + totals[:items_total] += line_item.amount + + sales_tax = tax_included_in line_item + + if sales_tax > 0 + totals[:taxable_total] += line_item.amount + totals[:sales_tax] += sales_tax + end + end + + totals.each_pair do |k, v| + totals[k] = totals[k].round(2) + end + + totals + end + + def shipping_cost_for(order) + shipping_cost = order.adjustments.find_by_label("Shipping").andand.amount + shipping_cost = shipping_cost.nil? ? 0.0 : shipping_cost + end + + def tax_included_in(line_item) + line_item.adjustments.sum &:included_tax + end + + def shipment_inc_vat + Spree::Config.shipment_inc_vat + end + + def shipping_tax_rate + Spree::Config.shipping_tax_rate + end + end +end diff --git a/lib/spree/api/testing_support/setup.rb b/lib/spree/api/testing_support/setup.rb index cbf8393e33..2b96b60946 100644 --- a/lib/spree/api/testing_support/setup.rb +++ b/lib/spree/api/testing_support/setup.rb @@ -7,6 +7,7 @@ module Spree user = stub_model(Spree::LegacyUser) user.stub(:has_spree_role?).with("admin").and_return(false) user.stub(:enterprises) { [] } + user.stub(:owned_groups) { [] } user end end @@ -33,6 +34,7 @@ module Spree # Stub enterprises, needed for cancan ability checks user.stub(:enterprises) { [] } + user.stub(:owned_groups) { [] } user end diff --git a/lib/tasks/data.rake b/lib/tasks/data.rake new file mode 100644 index 0000000000..23db3e6049 --- /dev/null +++ b/lib/tasks/data.rake @@ -0,0 +1,90 @@ +namespace :openfoodnetwork do + namespace :data do + desc "Adding relationships based on recent order cycles" + task :create_order_cycle_relationships => :environment do + input = request_months + + # For each order cycle which was modified within the past 3 months + OrderCycle.where('updated_at > ?', Date.today - input.months).each do |order_cycle| + # Cycle through the incoming exchanges + order_cycle.exchanges.incoming.each do |exchange| + unless exchange.sender == exchange.receiver + # Ensure that an enterprise relationship from the producer to the coordinator exists + relationship = EnterpriseRelationship.where(parent_id: exchange.sender_id, child_id: exchange.receiver_id).first + unless relationship.present? + puts "CREATING: #{exchange.sender.name} TO #{exchange.receiver.name}" + relationship = EnterpriseRelationship.create!(parent_id: exchange.sender_id, child_id: exchange.receiver_id) + end + # And that P-OC is granted + unless relationship.has_permission?(:add_to_order_cycle) + puts "PERMITTING: #{exchange.sender.name} TO #{exchange.receiver.name}" + relationship.permissions.create!(name: :add_to_order_cycle) + end + end + end + + # Cycle through the outgoing exchanges + order_cycle.exchanges.outgoing.each do |exchange| + unless exchange.sender == exchange.receiver + # Enure that an enterprise relationship from the hub to the coordinator exists + relationship = EnterpriseRelationship.where(parent_id: exchange.receiver_id, child_id: exchange.sender_id).first + unless relationship.present? + puts "CREATING: #{exchange.receiver.name} TO #{exchange.sender.name}" + relationship = EnterpriseRelationship.create!(parent_id: exchange.receiver_id, child_id: exchange.sender_id) + end + # And that P-OC is granted + unless relationship.has_permission?(:add_to_order_cycle) + puts "PERMITTING: #{exchange.receiver.name} TO #{exchange.sender.name}" + relationship.permissions.create!(name: :add_to_order_cycle) + end + end + + # For each variant in the exchange + products = Spree::Product.joins(:variants_including_master).where('spree_variants.id IN (?)', exchange.variants).pluck(:id).uniq + producers = Enterprise.joins(:supplied_products).where("spree_products.id IN (?)", products).uniq + producers.each do |producer| + unless producer == exchange.receiver + # Ensure that an enterprise relationship from the producer to the hub exists + relationship = EnterpriseRelationship.where(parent_id: producer.id, child_id: exchange.receiver_id).first + unless relationship.present? + puts "CREATING: #{producer.name} TO #{exchange.receiver.name}" + relationship = EnterpriseRelationship.create!(parent_id: producer.id, child_id: exchange.receiver_id) + end + # And that P-OC is granted + unless relationship.has_permission?(:add_to_order_cycle) + puts "PERMITTING: #{producer.name} TO #{exchange.receiver.name}" + relationship.permissions.create!(name: :add_to_order_cycle) + end + end + end + end + end + end + + def request_months + # Ask how many months back we want to search for + puts "This task will search order cycle edited within (n) months of today's date.\nPlease enter a value for (n), or hit ENTER to use the default of three (3) months." + input = check_default(STDIN.gets.chomp) + + while !is_integer?(input) + puts "'#{input}' is not an integer. Please enter an integer." + input = check_default(STDIN.gets.chomp) + end + + Integer(input) + end + + def check_default(input) + if input.blank? + puts "Using default value of three (3) months." + 3 + else + input + end + end + + def is_integer?(value) + return true if Integer(value) rescue false + end + end +end diff --git a/script/delayed_job b/script/delayed_job new file mode 100755 index 0000000000..edf195985f --- /dev/null +++ b/script/delayed_job @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby + +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) +require 'delayed/command' +Delayed::Command.new(ARGV).daemonize diff --git a/script/push_to_production.sh b/script/push_to_production.sh new file mode 100755 index 0000000000..b4e374d744 --- /dev/null +++ b/script/push_to_production.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +PROD_TEST=`git remote | grep -s 'production' || true` +if [[ "$PROD_TEST" != *production* ]]; then + git remote add production ubuntu@ofn-prod:apps/openfoodweb/current +fi + +[[ $(git push production $BUILDKITE_COMMIT:master --force 2>&1) =~ "Done" ]] diff --git a/script/push_to_staging.sh b/script/push_to_staging.sh new file mode 100755 index 0000000000..e3a16ebb3b --- /dev/null +++ b/script/push_to_staging.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +ST2_TEST=`git remote | grep -s 'staging2' || true` +if [[ "$ST2_TEST" != *staging2* ]]; then + git remote add staging2 openfoodweb@ofn-staging2:apps/openfoodweb/current +fi + +[[ $(git push staging2 $BUILDKITE_COMMIT:master --force 2>&1) =~ "Done" ]] diff --git a/script/run_tests.sh b/script/run_tests.sh new file mode 100755 index 0000000000..9082a79190 --- /dev/null +++ b/script/run_tests.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e + +echo "--- Loading environment" +source /var/lib/jenkins/.rvm/environments/ruby-1.9.3-p392 +if [ ! -f config/application.yml ]; then + ln -s application.yml.example config/application.yml +fi + +echo "--- Bundling" +bundle install + +echo "--- Preparing test database" +bundle exec rake db:test:prepare + +echo "--- Running tests" +bundle exec rspec spec diff --git a/spec/controllers/admin/enterprises_controller_spec.rb b/spec/controllers/admin/enterprises_controller_spec.rb index 5b3b182842..75a0dc1a4b 100644 --- a/spec/controllers/admin/enterprises_controller_spec.rb +++ b/spec/controllers/admin/enterprises_controller_spec.rb @@ -1,33 +1,26 @@ require 'spec_helper' +require 'open_food_network/order_cycle_permissions' module Admin describe EnterprisesController do include AuthenticationWorkflow - let(:user) { create_enterprise_user } - let(:distributor_manager) do - user = create(:user) - user.spree_roles = [] - distributor.enterprise_roles.build(user: user).save - user - end - let(:distributor_owner) do - user = create(:user) - user.spree_roles = [] - user - end - let(:admin_user) do - user = create(:user) - user.spree_roles << Spree::Role.find_or_create_by_name!('admin') - user - end + + let(:user) { create(:user) } + let(:admin_user) { create(:admin_user) } + let(:distributor_manager) { create(:user, enterprise_limit: 10, enterprises: [distributor]) } + let(:supplier_manager) { create(:user, enterprise_limit: 10, enterprises: [supplier]) } + let(:distributor_owner) { create(:user, enterprise_limit: 10) } + let(:supplier_owner) { create(:user) } let(:distributor) { create(:distributor_enterprise, owner: distributor_owner ) } + let(:supplier) { create(:supplier_enterprise, owner: supplier_owner) } + before { @request.env['HTTP_REFERER'] = 'http://test.com/' } describe "creating an enterprise" do let(:country) { Spree::Country.find_by_name 'Australia' } let(:state) { Spree::State.find_by_name 'Victoria' } - let(:enterprise_params) { {enterprise: {name: 'zzz', permalink: 'zzz', email: "bob@example.com", address_attributes: {address1: 'a', city: 'a', zipcode: 'a', country_id: country.id, state_id: state.id}}} } + let(:enterprise_params) { {enterprise: {name: 'zzz', permalink: 'zzz', is_primary_producer: '0', email: "bob@example.com", address_attributes: {address1: 'a', city: 'a', zipcode: 'a', country_id: country.id, state_id: state.id}}} } it "grants management permission if the current user is an enterprise user" do controller.stub spree_current_user: distributor_manager @@ -47,7 +40,7 @@ module Admin admin_user.enterprise_roles.where(enterprise_id: enterprise).should be_empty end - it "it overrides the owner_id submitted by the user unless current_user is super admin" do + it "overrides the owner_id submitted by the user unless current_user is super admin" do controller.stub spree_current_user: distributor_manager enterprise_params[:enterprise][:owner_id] = user @@ -55,6 +48,62 @@ module Admin enterprise = Enterprise.find_by_name 'zzz' distributor_manager.enterprise_roles.where(enterprise_id: enterprise).first.should be end + + context "when I already own a hub" do + before { distributor } + + it "creates new non-producers as hubs" do + controller.stub spree_current_user: distributor_owner + enterprise_params[:enterprise][:owner_id] = distributor_owner + + spree_put :create, enterprise_params + enterprise = Enterprise.find_by_name 'zzz' + enterprise.sells.should == 'any' + end + + it "creates new producers as sells none" do + controller.stub spree_current_user: distributor_owner + enterprise_params[:enterprise][:owner_id] = distributor_owner + enterprise_params[:enterprise][:is_primary_producer] = '1' + + spree_put :create, enterprise_params + enterprise = Enterprise.find_by_name 'zzz' + enterprise.sells.should == 'none' + end + + it "doesn't affect the hub status for super admins" do + admin_user.enterprises << create(:distributor_enterprise) + + controller.stub spree_current_user: admin_user + enterprise_params[:enterprise][:owner_id] = admin_user + enterprise_params[:enterprise][:sells] = 'none' + + spree_put :create, enterprise_params + enterprise = Enterprise.find_by_name 'zzz' + enterprise.sells.should == 'none' + end + end + + context "when I do not have a hub" do + it "does not create the new enterprise as a hub" do + controller.stub spree_current_user: supplier_manager + enterprise_params[:enterprise][:owner_id] = supplier_manager + + spree_put :create, enterprise_params + enterprise = Enterprise.find_by_name 'zzz' + enterprise.sells.should == 'none' + end + + it "doesn't affect the hub status for super admins" do + controller.stub spree_current_user: admin_user + enterprise_params[:enterprise][:owner_id] = admin_user + enterprise_params[:enterprise][:sells] = 'any' + + spree_put :create, enterprise_params + enterprise = Enterprise.find_by_name 'zzz' + enterprise.sells.should == 'any' + end + end end describe "updating an enterprise" do @@ -88,6 +137,50 @@ module Admin distributor.reload expect(distributor.users).to_not include user end + + + describe "enterprise properties" do + let(:producer) { create(:enterprise) } + let!(:property) { create(:property, name: "A nice name") } + + before do + login_as_enterprise_user [producer] + end + + context "when a submitted property does not already exist" do + it "does not create a new property, or product property" do + spree_put :update, { + id: producer, + enterprise: { + producer_properties_attributes: { + '0' => { property_name: 'a different name', value: 'something' } + } + } + } + expect(Spree::Property.count).to be 1 + expect(ProducerProperty.count).to be 0 + property_names = producer.reload.properties.map(&:name) + expect(property_names).to_not include 'a different name' + end + end + + context "when a submitted property exists" do + it "adds a product property" do + spree_put :update, { + id: producer, + enterprise: { + producer_properties_attributes: { + '0' => { property_name: 'A nice name', value: 'something' } + } + } + } + expect(Spree::Property.count).to be 1 + expect(ProducerProperty.count).to be 1 + property_names = producer.reload.properties.map(&:name) + expect(property_names).to include 'A nice name' + end + end + end end context "as owner" do @@ -190,12 +283,14 @@ module Admin end it "is disallowed" do - spree_post :set_sells, { id: enterprise, sells: 'own' } - expect(response).to redirect_to spree.admin_path - trial_expiry = Date.today.strftime("%Y-%m-%d") - expect(flash[:error]).to eq "Sorry, but you've already had a trial. Expired on: #{trial_expiry}" - expect(enterprise.reload.sells).to eq 'own' - expect(enterprise.reload.shop_trial_start_date).to eq (Date.today - 30.days).to_time + Timecop.freeze(Time.zone.local(2015, 4, 16, 14, 0, 0)) do + spree_post :set_sells, { id: enterprise, sells: 'own' } + expect(response).to redirect_to spree.admin_path + trial_expiry = Date.today.strftime("%Y-%m-%d") + expect(flash[:error]).to eq "Sorry, but you've already had a trial. Expired on: #{trial_expiry}" + expect(enterprise.reload.sells).to eq 'own' + expect(enterprise.reload.shop_trial_start_date).to eq (Date.today - 30.days).to_time + end end end @@ -313,5 +408,51 @@ module Admin end end end + + describe "for_order_cycle" do + let!(:user) { create_enterprise_user } + let!(:enterprise) { create(:enterprise, sells: 'any', owner: user) } + let(:permission_mock) { double(:permission) } + + before do + # As a user with permission + controller.stub spree_current_user: user + OrderCycle.stub find_by_id: "existing OrderCycle" + Enterprise.stub find_by_id: "existing Enterprise" + OrderCycle.stub new: "new OrderCycle" + + allow(OpenFoodNetwork::OrderCyclePermissions).to receive(:new) { permission_mock } + allow(permission_mock).to receive(:visible_enterprises) { [] } + allow(ActiveModel::ArraySerializer).to receive(:new) { "" } + end + + context "when no order_cycle or coordinator is provided in params" do + before { spree_get :for_order_cycle, format: :json } + it "initializes permissions with nil" do + expect(OpenFoodNetwork::OrderCyclePermissions).to have_received(:new).with(user, nil) + end + end + + context "when an order_cycle_id is provided in params" do + before { spree_get :for_order_cycle, format: :json, order_cycle_id: 1 } + it "initializes permissions with the existing OrderCycle" do + expect(OpenFoodNetwork::OrderCyclePermissions).to have_received(:new).with(user, "existing OrderCycle") + end + end + + context "when a coordinator is provided in params" do + before { spree_get :for_order_cycle, format: :json, coordinator_id: 1 } + it "initializes permissions with a new OrderCycle" do + expect(OpenFoodNetwork::OrderCyclePermissions).to have_received(:new).with(user, "new OrderCycle") + end + end + + context "when both an order cycle and a coordinator are provided in params" do + before { spree_get :for_order_cycle, format: :json, order_cycle_id: 1, coordinator_id: 1 } + it "initializes permissions with the existing OrderCycle" do + expect(OpenFoodNetwork::OrderCyclePermissions).to have_received(:new).with(user, "existing OrderCycle") + end + end + end end end diff --git a/spec/controllers/admin/order_cycles_controller_spec.rb b/spec/controllers/admin/order_cycles_controller_spec.rb new file mode 100644 index 0000000000..9e996f0da7 --- /dev/null +++ b/spec/controllers/admin/order_cycles_controller_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper' + +module Admin + describe OrderCyclesController do + include AuthenticationWorkflow + let!(:distributor_owner) { create_enterprise_user enterprise_limit: 2 } + + before do + controller.stub spree_current_user: distributor_owner + end + + describe "new" do + describe "when the user manages no distributor enterprises suitable for coordinator" do + let!(:distributor) { create(:distributor_enterprise, owner: distributor_owner, confirmed_at: nil) } + + it "redirects to order cycles index" do + spree_get :new + expect(response).to redirect_to admin_order_cycles_path + end + end + + describe "when the user manages a single distributor enterprise suitable for coordinator" do + let!(:distributor) { create(:distributor_enterprise, owner: distributor_owner) } + + it "renders the new template" do + spree_get :new + expect(response).to render_template :new + end + end + + describe "when a user manages multiple enterprises suitable for coordinator" do + let!(:distributor1) { create(:distributor_enterprise, owner: distributor_owner) } + let!(:distributor2) { create(:distributor_enterprise, owner: distributor_owner) } + let!(:distributor3) { create(:distributor_enterprise) } + + it "renders the set_coordinator template" do + spree_get :new + expect(response).to render_template :set_coordinator + end + + describe "and a coordinator_id is submitted as part of the request" do + describe "when the user manages the enterprise" do + it "renders the new template" do + spree_get :new, coordinator_id: distributor1.id + expect(response).to render_template :new + end + end + + describe "when the user does not manage the enterprise" do + it "renders the set_coordinator template and sets a flash error" do + spree_get :new, coordinator_id: distributor3.id + expect(response).to render_template :set_coordinator + expect(flash[:error]).to eq "You don't have permission to create an order cycle coordinated by that enterprise" + end + end + end + end + end + + describe "bulk_update" do + let(:oc) { create(:simple_order_cycle) } + let!(:coordinator) { oc.coordinator } + + context "when I manage the coordinator of an order cycle" do + before { create(:enterprise_role, user: distributor_owner, enterprise: coordinator) } + + it "updates order cycle properties" do + spree_put :bulk_update, order_cycle_set: { collection_attributes: { '0' => { + id: oc.id, + orders_open_at: Date.today - 21.days, + orders_close_at: Date.today + 21.days, + } } } + + oc.reload + expect(oc.orders_open_at.to_date).to eq Date.today - 21.days + expect(oc.orders_close_at.to_date).to eq Date.today + 21.days + end + end + + context "when I do not manage the coordinator of an order cycle" do + # I need to manage a hub in order to access the bulk_update action + let!(:another_distributor) { create(:distributor_enterprise, users: [distributor_owner]) } + + it "doesn't update order cycle properties" do + spree_put :bulk_update, order_cycle_set: { collection_attributes: { '0' => { + id: oc.id, + orders_open_at: Date.today - 21.days, + orders_close_at: Date.today + 21.days, + } } } + + oc.reload + expect(oc.orders_open_at.to_date).to_not eq Date.today - 21.days + expect(oc.orders_close_at.to_date).to_not eq Date.today + 21.days + end + end + end + + describe "destroy" do + let!(:distributor) { create(:distributor_enterprise, owner: distributor_owner) } + + describe "when an order cycle becomes non-deletable, and we attempt to delete it" do + let!(:oc) { create(:simple_order_cycle, coordinator: distributor) } + let!(:order) { create(:order, order_cycle: oc) } + + before { spree_get :destroy, id: oc.id } + + it "displays an error message" do + expect(response).to redirect_to admin_order_cycles_path + expect(flash[:error]).to eq "That order cycle has been selected by a customer and cannot be deleted. To prevent customers from accessing it, please close it instead." + end + end + end + end +end diff --git a/spec/controllers/api/enterprises_controller_spec.rb b/spec/controllers/api/enterprises_controller_spec.rb index e21a879d4f..77e16368d9 100644 --- a/spec/controllers/api/enterprises_controller_spec.rb +++ b/spec/controllers/api/enterprises_controller_spec.rb @@ -12,7 +12,29 @@ module Api Enterprise.stub(:find).and_return(enterprise) end - describe "as an enterprise manager" do + context "as an enterprise owner" do + let(:enterprise_owner) { create_enterprise_user enterprise_limit: 10 } + let(:enterprise) { create(:distributor_enterprise, owner: enterprise_owner) } + + before do + Spree.user_class.stub :find_by_spree_api_key => enterprise_owner + end + + describe "creating an enterprise" do + let(:australia) { Spree::Country.find_by_name('Australia') } + let(:new_enterprise_params) { {enterprise: {name: 'name', email: 'email@example.com', address_attributes: {address1: '123 Abc Street', city: 'Northcote', zipcode: '3070', state_id: australia.states.first, country_id: australia.id } } } } + + it "creates as sells=any when it is not a producer" do + spree_post :create, new_enterprise_params + response.should be_success + + enterprise = Enterprise.last + enterprise.sells.should == 'any' + end + end + end + + context "as an enterprise manager" do let(:enterprise_manager) { create_enterprise_user } before do diff --git a/spec/controllers/spree/admin/products_controller_spec.rb b/spec/controllers/spree/admin/products_controller_spec.rb index 9746ffa0e2..b8d76abd9c 100644 --- a/spec/controllers/spree/admin/products_controller_spec.rb +++ b/spec/controllers/spree/admin/products_controller_spec.rb @@ -63,4 +63,53 @@ describe Spree::Admin::ProductsController do response.should redirect_to "/admin/products/new" end end + + describe "updating" do + describe "product properties" do + context "as an enterprise user" do + let(:producer) { create(:enterprise) } + let!(:product) { create(:simple_product, supplier: producer) } + let!(:property) { create(:property, name: "A nice name") } + + before do + @request.env['HTTP_REFERER'] = 'http://test.com/' + login_as_enterprise_user [producer] + end + + context "when a submitted property does not already exist" do + it "does not create a new property, or product property" do + spree_put :update, { + id: product, + product: { + product_properties_attributes: { + '0' => { property_name: 'a different name', value: 'something' } + } + } + } + expect(Spree::Property.count).to be 1 + expect(Spree::ProductProperty.count).to be 0 + property_names = product.reload.properties.map(&:name) + expect(property_names).to_not include 'a different name' + end + end + + context "when a submitted property exists" do + it "adds a product property" do + spree_put :update, { + id: product, + product: { + product_properties_attributes: { + '0' => { property_name: 'A nice name', value: 'something' } + } + } + } + expect(Spree::Property.count).to be 1 + expect(Spree::ProductProperty.count).to be 1 + property_names = product.reload.properties.map(&:name) + expect(property_names).to include 'A nice name' + end + end + end + end + end end diff --git a/spec/factories.rb b/spec/factories.rb index 33d760ec15..aa86af8563 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -137,6 +137,7 @@ FactoryGirl.define do name 'Enterprise group' description 'this is a group' on_front_page false + address { FactoryGirl.build(:address) } end sequence(:calculator_amount) @@ -181,6 +182,34 @@ FactoryGirl.define do order.reload end end + + factory :zone_with_member, :parent => :zone do + default_tax true + + after(:create) do |zone| + Spree::ZoneMember.create!(zone: zone, zoneable: Spree::Country.find_by_name('Australia')) + end + end + + factory :taxed_product, :parent => :product do + ignore do + tax_rate_amount 0 + zone nil + end + + tax_category { create(:tax_category) } + + after(:create) do |product, proxy| + create(:tax_rate, amount: proxy.tax_rate_amount, tax_category: product.tax_category, included_in_price: true, calculator: Spree::Calculator::DefaultTax.new, zone: proxy.zone) + end + end + + factory :customer, :class => Customer do + email { Faker::Internet.email } + enterprise + code 'abc123' + user + end end @@ -218,7 +247,7 @@ FactoryGirl.modify do country { Spree::Country.find_by_name 'Australia' || Spree::Country.first } end - factory :payment do + factory :payment do ignore do distributor { order.distributor || Enterprise.is_distributor.first || FactoryGirl.create(:distributor_enterprise) } end @@ -233,6 +262,18 @@ FactoryGirl.modify do # Prevent inconsistent ordering in specs when all option types have the same (0) position sequence(:position) end + + factory :user do + after(:create) do |user| + user.spree_roles.clear # Remove admin role + end + end + + factory :admin_user do + after(:create) do |user| + user.spree_roles << Spree::Role.find_or_create_by_name!('admin') + end + end end diff --git a/spec/features/admin/authentication_spec.rb b/spec/features/admin/authentication_spec.rb index beb8c31c2b..059963fd70 100644 --- a/spec/features/admin/authentication_spec.rb +++ b/spec/features/admin/authentication_spec.rb @@ -2,7 +2,11 @@ require 'spec_helper' feature "Authentication", js: true do include UIComponentHelper + include AuthenticationWorkflow + include WebHelper + let(:user) { create(:user, password: "password", password_confirmation: "password") } + let!(:enterprise) { create(:enterprise, owner: user) } # Required for access to admin scenario "logging into admin redirects home, then back to admin" do # This is the first admin spec, so give a little extra load time for slow systems @@ -13,7 +17,13 @@ feature "Authentication", js: true do fill_in "Email", with: user.email fill_in "Password", with: user.password click_login_button - page.should have_content "Dashboard" + page.should have_content "DASHBOARD" current_path.should == spree.admin_path end + + scenario "viewing my account" do + login_to_admin_section + click_link "Account" + current_path.should == spree.account_path + end end diff --git a/spec/features/admin/cms_spec.rb b/spec/features/admin/cms_spec.rb index 29fc2412e2..e3dfcf19b9 100644 --- a/spec/features/admin/cms_spec.rb +++ b/spec/features/admin/cms_spec.rb @@ -18,13 +18,13 @@ feature %q{ current_path.should match(/^\/admin/) end - scenario "anonymous user can't access CMS admin" do + scenario "anonymous user can't access CMS admin", js: true do visit cms_admin_path page.should_not have_content "ComfortableMexicanSofa" - page.should have_content "Login" + page.should have_content "Log in" end - scenario "non-admin user can't access CMS admin" do + scenario "non-admin user can't access CMS admin", js: true do login_to_consumer_section visit cms_admin_path page.should_not have_content "ComfortableMexicanSofa" diff --git a/spec/features/admin/enterprise_fees_spec.rb b/spec/features/admin/enterprise_fees_spec.rb index 44f26deddd..fe55d8e90b 100644 --- a/spec/features/admin/enterprise_fees_spec.rb +++ b/spec/features/admin/enterprise_fees_spec.rb @@ -6,9 +6,11 @@ feature %q{ }, js: true do include AuthenticationWorkflow include WebHelper - + + let!(:tax_category_gst) { create(:tax_category, name: 'GST') } + scenario "listing enterprise fees" do - fee = create(:enterprise_fee, name: '$0.50 / kg', fee_type: 'packing') + fee = create(:enterprise_fee, name: '$0.50 / kg', fee_type: 'packing', tax_category: tax_category_gst) amount = fee.calculator.preferred_amount login_to_admin_section @@ -18,6 +20,7 @@ feature %q{ page.should have_selector "#enterprise_fee_set_collection_attributes_0_enterprise_id" page.should have_selector "option[selected]", text: 'Packing' page.should have_selector "input[value='$0.50 / kg']" + page.should have_selector "option[selected]", text: 'GST' page.should have_selector "option[selected]", text: 'Flat Rate (per item)' page.should have_selector "input[value='#{amount}']" end @@ -35,6 +38,7 @@ feature %q{ select 'Feedme', from: 'enterprise_fee_set_collection_attributes_0_enterprise_id' select 'Admin', from: 'enterprise_fee_set_collection_attributes_0_fee_type' fill_in 'enterprise_fee_set_collection_attributes_0_name', with: 'Hello!' + select 'GST', from: 'enterprise_fee_set_collection_attributes_0_tax_category_id' select 'Flat Percent', from: 'enterprise_fee_set_collection_attributes_0_calculator_type' click_button 'Update' @@ -64,6 +68,7 @@ feature %q{ select 'Foo', from: 'enterprise_fee_set_collection_attributes_0_enterprise_id' select 'Admin', from: 'enterprise_fee_set_collection_attributes_0_fee_type' fill_in 'enterprise_fee_set_collection_attributes_0_name', with: 'Greetings!' + select '', from: 'enterprise_fee_set_collection_attributes_0_tax_category_id' select 'Flat Percent', from: 'enterprise_fee_set_collection_attributes_0_calculator_type' click_button 'Update' @@ -71,6 +76,7 @@ feature %q{ page.should have_selector "option[selected]", text: 'Foo' page.should have_selector "option[selected]", text: 'Admin' page.should have_selector "input[value='Greetings!']" + page.should have_select 'enterprise_fee_set_collection_attributes_0_tax_category_id', selected: '' page.should have_selector "option[selected]", text: 'Flat Percent' end @@ -137,6 +143,7 @@ feature %q{ select distributor1.name, :from => 'enterprise_fee_set_collection_attributes_0_enterprise_id' fill_in 'enterprise_fee_set_collection_attributes_0_name', :with => 'foo' + select 'GST', from: 'enterprise_fee_set_collection_attributes_0_tax_category_id' select 'Flat Percent', :from => 'enterprise_fee_set_collection_attributes_0_calculator_type' click_button 'Update' diff --git a/spec/features/admin/enterprise_groups_spec.rb b/spec/features/admin/enterprise_groups_spec.rb index 24e89e9be7..59712f3bfe 100644 --- a/spec/features/admin/enterprise_groups_spec.rb +++ b/spec/features/admin/enterprise_groups_spec.rb @@ -15,28 +15,33 @@ feature %q{ e = create(:enterprise) group = create(:enterprise_group, enterprises: [e], on_front_page: true) - click_link 'Configuration' - click_link 'Enterprise Groups' + click_link 'Groups' page.should have_selector 'td', text: group.name page.should have_selector 'td', text: 'Y' page.should have_selector 'td', text: e.name end - scenario "creating a new enterprise group" do + scenario "creating a new enterprise group", js: true do e1 = create(:enterprise) e2 = create(:enterprise) e3 = create(:enterprise) - click_link 'Configuration' - click_link 'Enterprise Groups' + click_link 'Groups' click_link 'New Enterprise Group' fill_in 'enterprise_group_name', with: 'EGEGEG' fill_in 'enterprise_group_description', with: 'This is a description' check 'enterprise_group_on_front_page' - select e1.name, from: 'enterprise_group_enterprise_ids' - select e2.name, from: 'enterprise_group_enterprise_ids' + select2_search e1.name, from: 'Enterprises' + select2_search e2.name, from: 'Enterprises' + click_link 'Contact' + fill_in 'enterprise_group_address_attributes_phone', with: '000' + fill_in 'enterprise_group_address_attributes_address1', with: 'My Street' + fill_in 'enterprise_group_address_attributes_city', with: 'Block' + fill_in 'enterprise_group_address_attributes_zipcode', with: '0000' + select2_search 'Australia', :from => 'Country' + select2_search 'Victoria', :from => 'State' click_button 'Create' page.should have_content 'Enterprise group "EGEGEG" has been successfully created!' @@ -53,8 +58,7 @@ feature %q{ e2 = create(:enterprise) eg = create(:enterprise_group, name: 'EGEGEG', on_front_page: true, enterprises: [e1, e2]) - click_link 'Configuration' - click_link 'Enterprise Groups' + click_link 'Groups' first("a.edit-enterprise-group").click page.should have_field 'enterprise_group_name', with: 'EGEGEG' @@ -80,8 +84,7 @@ feature %q{ eg1 = create(:enterprise_group, name: 'A') eg2 = create(:enterprise_group, name: 'B') - click_link 'Configuration' - click_link 'Enterprise Groups' + click_link 'Groups' page.all('td.name').map(&:text).should == ['A', 'B'] all("a.move-down").first.click @@ -93,8 +96,7 @@ feature %q{ scenario "deleting an enterprise group", js: true do eg = create(:enterprise_group, name: 'EGEGEG') - click_link 'Configuration' - click_link 'Enterprise Groups' + click_link 'Groups' first("a.delete-resource").click page.should have_no_content 'EGEGEG' diff --git a/spec/features/admin/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index 0bd9b8dfb5..a75125bdb5 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -368,17 +368,18 @@ feature %q{ expect(find("#content-header")).to have_link "New Enterprise" end + end - context "when I have reached my enterprise ownership limit" do - it "does not display the link to create a new enterprise" do - enterprise_user.owned_enterprises.push [supplier1] + context "when I have reached my enterprise ownership limit", js: true do + it "does not display the link to create a new enterprise" do + supplier1.reload + enterprise_user.owned_enterprises.push [supplier1] - click_link "Enterprises" + click_link "Enterprises" - page.should have_content supplier1.name - page.should have_content distributor1.name - expect(find("#content-header")).to_not have_link "New Enterprise" - end + page.should have_content supplier1.name + page.should have_content distributor1.name + expect(find("#content-header")).to_not have_link "New Enterprise" end end @@ -473,11 +474,12 @@ feature %q{ end scenario "managing producer properties", js: true do + create(:property, name: "Certified Organic") click_link 'Enterprises' within(".enterprise-#{supplier1.id}") { click_link 'Properties' } - # -- Create / update - fill_in 'enterprise_producer_properties_attributes_0_property_name', with: "Certified Organic" + # -- Update only + select2_select "Certified Organic", from: 'enterprise_producer_properties_attributes_0_property_name' fill_in 'enterprise_producer_properties_attributes_0_value', with: "NASAA 12345" click_button 'Update' page.should have_selector '#listing_enterprises a', text: supplier1.name diff --git a/spec/features/admin/order_cycles_spec.rb b/spec/features/admin/order_cycles_spec.rb index 180485a734..cb96799fbb 100644 --- a/spec/features/admin/order_cycles_spec.rb +++ b/spec/features/admin/order_cycles_spec.rb @@ -67,6 +67,11 @@ feature %q{ v2 = create(:variant, product: product) distributor = create(:distributor_enterprise, name: 'My distributor', with_payment_and_shipping: true) + # Relationships required for interface to work + create(:enterprise_relationship, parent: supplier, child: coordinator, permissions_list: [:add_to_order_cycle]) + create(:enterprise_relationship, parent: distributor, child: coordinator, permissions_list: [:add_to_order_cycle]) + create(:enterprise_relationship, parent: supplier, child: distributor, permissions_list: [:add_to_order_cycle]) + # And some enterprise fees supplier_fee = create(:enterprise_fee, enterprise: supplier, name: 'Supplier fee') coordinator_fee = create(:enterprise_fee, enterprise: coordinator, name: 'Coord fee') @@ -77,11 +82,14 @@ feature %q{ click_link 'Order Cycles' click_link 'New Order Cycle' + # Select a coordinator since there are two available + select2_select 'My coordinator', from: 'coordinator_id' + click_button "Continue >" + # And I fill in the basic fields fill_in 'order_cycle_name', with: 'Plums & Avos' - fill_in 'order_cycle_orders_open_at', with: '2012-11-06 06:00:00' - fill_in 'order_cycle_orders_close_at', with: '2012-11-13 17:00:00' - select 'My coordinator', from: 'order_cycle_coordinator_id' + fill_in 'order_cycle_orders_open_at', with: '2040-11-06 06:00:00' + fill_in 'order_cycle_orders_close_at', with: '2040-11-13 17:00:00' # And I add a coordinator fee click_button 'Add coordinator fee' @@ -123,8 +131,8 @@ feature %q{ page.should have_selector 'a', text: 'Plums & Avos' - page.should have_selector "input[value='2012-11-06 06:00:00 +1100']" - page.should have_selector "input[value='2012-11-13 17:00:00 +1100']" + page.should have_selector "input[value='2040-11-06 06:00:00 +1100']" + page.should have_selector "input[value='2040-11-13 17:00:00 +1100']" page.should have_content 'My coordinator' page.should have_selector 'td.suppliers', text: 'My supplier' @@ -161,8 +169,7 @@ feature %q{ page.find('#order_cycle_name').value.should == oc.name page.find('#order_cycle_orders_open_at').value.should == oc.orders_open_at.to_s page.find('#order_cycle_orders_close_at').value.should == oc.orders_close_at.to_s - page.find('#order_cycle_coordinator_id').value.to_i.should == oc.coordinator_id - page.should have_selector "select[name='order_cycle_coordinator_fee_0_id']" + page.should have_content "COORDINATOR #{oc.coordinator.name}" # And I should see the suppliers page.should have_selector 'td.supplier_name', :text => oc.suppliers.first.name @@ -245,13 +252,18 @@ feature %q{ initial_variants = oc.variants.sort_by &:id # And a coordinating, supplying and distributing enterprise with some products with variants - coordinator = create(:distributor_enterprise, name: 'My coordinator') + coordinator = oc.coordinator supplier = create(:supplier_enterprise, name: 'My supplier') distributor = create(:distributor_enterprise, name: 'My distributor', with_payment_and_shipping: true) product = create(:product, supplier: supplier) v1 = create(:variant, product: product) v2 = create(:variant, product: product) + # Relationships required for interface to work + create(:enterprise_relationship, parent: supplier, child: coordinator, permissions_list: [:add_to_order_cycle]) + create(:enterprise_relationship, parent: distributor, child: coordinator, permissions_list: [:add_to_order_cycle]) + create(:enterprise_relationship, parent: supplier, child: distributor, permissions_list: [:add_to_order_cycle]) + # And some enterprise fees supplier_fee1 = create(:enterprise_fee, enterprise: supplier, name: 'Supplier fee 1') supplier_fee2 = create(:enterprise_fee, enterprise: supplier, name: 'Supplier fee 2') @@ -268,9 +280,11 @@ feature %q{ # And I update it fill_in 'order_cycle_name', with: 'Plums & Avos' - fill_in 'order_cycle_orders_open_at', with: '2012-11-06 06:00:00' - fill_in 'order_cycle_orders_close_at', with: '2012-11-13 17:00:00' - select 'My coordinator', from: 'order_cycle_coordinator_id' + fill_in 'order_cycle_orders_open_at', with: '2040-11-06 06:00:00' + fill_in 'order_cycle_orders_close_at', with: '2040-11-13 17:00:00' + + # CAN'T CHANGE COORDINATOR ANYMORE + # select 'My coordinator', from: 'order_cycle_coordinator_id' # And I configure some coordinator fees click_button 'Add coordinator fee' @@ -332,9 +346,9 @@ feature %q{ page.should have_selector 'a', text: 'Plums & Avos' - page.should have_selector "input[value='2012-11-06 06:00:00 +1100']" - page.should have_selector "input[value='2012-11-13 17:00:00 +1100']" - page.should have_content 'My coordinator' + page.should have_selector "input[value='2040-11-06 06:00:00 +1100']" + page.should have_selector "input[value='2040-11-13 17:00:00 +1100']" + page.should have_content coordinator.name page.should have_selector 'td.suppliers', text: 'My supplier' page.should have_selector 'td.distributors', text: 'My distributor' @@ -370,18 +384,18 @@ feature %q{ # And I fill in some new opening/closing times and save them within("tr.order-cycle-#{oc1.id}") do - all('input').first.set '2012-12-01 12:00:00' - all('input').last.set '2012-12-01 12:00:01' + all('input').first.set '2040-12-01 12:00:00' + all('input').last.set '2040-12-01 12:00:01' end within("tr.order-cycle-#{oc2.id}") do - all('input').first.set '2012-12-01 12:00:02' - all('input').last.set '2012-12-01 12:00:03' + all('input').first.set '2040-12-01 12:00:02' + all('input').last.set '2040-12-01 12:00:03' end within("tr.order-cycle-#{oc3.id}") do - all('input').first.set '2012-12-01 12:00:04' - all('input').last.set '2012-12-01 12:00:05' + all('input').first.set '2040-12-01 12:00:04' + all('input').last.set '2040-12-01 12:00:05' end click_button 'Update' @@ -463,7 +477,6 @@ feature %q{ end context "as an enterprise user" do - let!(:supplier_managed) { create(:supplier_enterprise, name: 'Managed supplier') } let!(:supplier_unmanaged) { create(:supplier_enterprise, name: 'Unmanaged supplier') } let!(:supplier_permitted) { create(:supplier_enterprise, name: 'Permitted supplier') } @@ -473,139 +486,260 @@ feature %q{ let!(:distributor_managed_fee) { create(:enterprise_fee, enterprise: distributor_managed, name: 'Managed distributor fee') } let!(:shipping_method) { create(:shipping_method, distributors: [distributor_managed, distributor_unmanaged, distributor_permitted]) } let!(:payment_method) { create(:payment_method, distributors: [distributor_managed, distributor_unmanaged, distributor_permitted]) } - - let!(:supplier_permitted_relationship) do - create(:enterprise_relationship, parent: supplier_permitted, child: supplier_managed, - permissions_list: [:add_to_order_cycle]) - end - let!(:distributor_permitted_relationship) do - create(:enterprise_relationship, parent: distributor_permitted, child: distributor_managed, - permissions_list: [:add_to_order_cycle]) - end let!(:product_managed) { create(:product, supplier: supplier_managed) } let!(:product_permitted) { create(:product, supplier: supplier_permitted) } before do - @new_user = create_enterprise_user - @new_user.enterprise_roles.build(enterprise: supplier_managed).save - @new_user.enterprise_roles.build(enterprise: distributor_managed).save + # Relationships required for interface to work + # Both suppliers allow both managed distributor to distribute their products (and add them to the order cycle) + create(:enterprise_relationship, parent: supplier_managed, child: distributor_managed, permissions_list: [:add_to_order_cycle]) + create(:enterprise_relationship, parent: supplier_permitted, child: distributor_managed, permissions_list: [:add_to_order_cycle]) - login_to_admin_as @new_user + # Both suppliers allow permitted distributor to distribute their products + create(:enterprise_relationship, parent: supplier_managed, child: distributor_permitted, permissions_list: [:add_to_order_cycle]) + create(:enterprise_relationship, parent: supplier_permitted, child: distributor_permitted, permissions_list: [:add_to_order_cycle]) + + # Permitted distributor can be added to the order cycle + create(:enterprise_relationship, parent: distributor_permitted, child: distributor_managed, permissions_list: [:add_to_order_cycle]) end - scenario "viewing a list of order cycles I am coordinating" do - oc_user_coordinating = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_unmanaged], name: 'Order Cycle 1' } ) - oc_for_other_user = create(:simple_order_cycle, { coordinator: supplier_unmanaged, name: 'Order Cycle 2' } ) + context "that is a manager of the coordinator" do + before do + @new_user = create_enterprise_user + @new_user.enterprise_roles.build(enterprise: supplier_managed).save + @new_user.enterprise_roles.build(enterprise: distributor_managed).save - click_link "Order Cycles" - - # I should see only the order cycle I am coordinating - page.should have_content oc_user_coordinating.name - page.should_not have_content oc_for_other_user.name - - # The order cycle should show enterprises that I manage - page.should have_selector 'td.suppliers', text: supplier_managed.name - page.should have_selector 'td.distributors', text: distributor_managed.name - - # The order cycle should not show enterprises that I don't manage - page.should_not have_selector 'td.suppliers', text: supplier_unmanaged.name - page.should_not have_selector 'td.distributors', text: distributor_unmanaged.name - end - - scenario "creating a new order cycle" do - click_link "Order Cycles" - click_link 'New Order Cycle' - - fill_in 'order_cycle_name', with: 'My order cycle' - fill_in 'order_cycle_orders_open_at', with: '2012-11-06 06:00:00' - fill_in 'order_cycle_orders_close_at', with: '2012-11-13 17:00:00' - - select 'Managed supplier', from: 'new_supplier_id' - click_button 'Add supplier' - select 'Permitted supplier', from: 'new_supplier_id' - click_button 'Add supplier' - - select_incoming_variant supplier_managed, 0, product_managed.master - select_incoming_variant supplier_permitted, 1, product_permitted.master - - select 'Managed distributor', from: 'order_cycle_coordinator_id' - click_button 'Add coordinator fee' - select 'Managed distributor fee', from: 'order_cycle_coordinator_fee_0_id' - - select 'Managed distributor', from: 'new_distributor_id' - click_button 'Add distributor' - select 'Permitted distributor', from: 'new_distributor_id' - click_button 'Add distributor' - - # Should only have suppliers / distributors listed which the user is managing or - # has E2E permission to add products to order cycles - page.should_not have_select 'new_supplier_id', with_options: [supplier_unmanaged.name] - page.should_not have_select 'new_distributor_id', with_options: [distributor_unmanaged.name] - - [distributor_unmanaged.name, supplier_managed.name, supplier_unmanaged.name].each do |enterprise_name| - page.should_not have_select 'order_cycle_coordinator_id', with_options: [enterprise_name] + login_to_admin_as @new_user end - click_button 'Create' + scenario "viewing a list of order cycles I am coordinating" do + oc_user_coordinating = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_unmanaged], coordinator: distributor_managed, distributors: [distributor_managed, distributor_unmanaged], name: 'Order Cycle 1' } ) + oc_for_other_user = create(:simple_order_cycle, { coordinator: supplier_unmanaged, name: 'Order Cycle 2' } ) - flash_message.should == "Your order cycle has been created." - order_cycle = OrderCycle.find_by_name('My order cycle') - order_cycle.suppliers.sort.should == [supplier_managed, supplier_permitted].sort - order_cycle.coordinator.should == distributor_managed - order_cycle.distributors.sort.should == [distributor_managed, distributor_permitted].sort + click_link "Order Cycles" + + # I should see only the order cycle I am coordinating + page.should have_content oc_user_coordinating.name + page.should_not have_content oc_for_other_user.name + + # The order cycle should show all enterprises in the order cycle + page.should have_selector 'td.suppliers', text: supplier_managed.name + page.should have_selector 'td.distributors', text: distributor_managed.name + page.should have_selector 'td.suppliers', text: supplier_unmanaged.name + page.should have_selector 'td.distributors', text: distributor_unmanaged.name + end + + scenario "creating a new order cycle" do + click_link "Order Cycles" + click_link 'New Order Cycle' + + # We go straight through to the new form, because only one coordinator is available + + fill_in 'order_cycle_name', with: 'My order cycle' + fill_in 'order_cycle_orders_open_at', with: '2040-11-06 06:00:00' + fill_in 'order_cycle_orders_close_at', with: '2040-11-13 17:00:00' + + select 'Managed supplier', from: 'new_supplier_id' + click_button 'Add supplier' + select 'Permitted supplier', from: 'new_supplier_id' + click_button 'Add supplier' + + select_incoming_variant supplier_managed, 0, product_managed.master + select_incoming_variant supplier_permitted, 1, product_permitted.master + + click_button 'Add coordinator fee' + select 'Managed distributor fee', from: 'order_cycle_coordinator_fee_0_id' + + select 'Managed distributor', from: 'new_distributor_id' + click_button 'Add distributor' + select 'Permitted distributor', from: 'new_distributor_id' + click_button 'Add distributor' + + # Should only have suppliers / distributors listed which the user is managing or + # has E2E permission to add products to order cycles + page.should_not have_select 'new_supplier_id', with_options: [supplier_unmanaged.name] + page.should_not have_select 'new_distributor_id', with_options: [distributor_unmanaged.name] + + [distributor_unmanaged.name, supplier_managed.name, supplier_unmanaged.name].each do |enterprise_name| + page.should_not have_select 'order_cycle_coordinator_id', with_options: [enterprise_name] + end + + click_button 'Create' + + flash_message.should == "Your order cycle has been created." + order_cycle = OrderCycle.find_by_name('My order cycle') + order_cycle.suppliers.sort.should == [supplier_managed, supplier_permitted].sort + order_cycle.coordinator.should == distributor_managed + order_cycle.distributors.sort.should == [distributor_managed, distributor_permitted].sort + end + + scenario "editing an order cycle we can see (and for now, edit) all exchanges in the order cycle" do + # TODO: when we add the editable scope to variant permissions, we should test that + # exchanges with enterprises who have not granted P-OC to the coordinator are not + # editable, but at this point we cannot distiguish between visible and editable + # variants. + + oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: distributor_managed, distributors: [distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) + + visit edit_admin_order_cycle_path(oc) + + # I should not see exchanges for supplier_unmanaged or distributor_unmanaged + page.all('tr.supplier').count.should == 3 + page.all('tr.distributor').count.should == 3 + + # When I save, then those exchanges should remain + click_button 'Update' + page.should have_content "Your order cycle has been updated." + + oc.reload + oc.suppliers.sort.should == [supplier_managed, supplier_permitted, supplier_unmanaged].sort + oc.coordinator.should == distributor_managed + oc.distributors.sort.should == [distributor_managed, distributor_permitted, distributor_unmanaged].sort + end + + scenario "editing an order cycle" do + oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: distributor_managed, distributors: [distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) + + visit edit_admin_order_cycle_path(oc) + + # When I remove all the exchanges and save + page.find("tr.supplier-#{supplier_managed.id} a.remove-exchange").click + page.find("tr.supplier-#{supplier_permitted.id} a.remove-exchange").click + page.find("tr.distributor-#{distributor_managed.id} a.remove-exchange").click + page.find("tr.distributor-#{distributor_permitted.id} a.remove-exchange").click + click_button 'Update' + + # Then the exchanges should be removed + page.should have_content "Your order cycle has been updated." + + oc.reload + oc.suppliers.should == [supplier_unmanaged] + oc.coordinator.should == distributor_managed + oc.distributors.should == [distributor_unmanaged] + end + + scenario "cloning an order cycle" do + oc = create(:simple_order_cycle, coordinator: distributor_managed) + + click_link "Order Cycles" + first('a.clone-order-cycle').click + flash_message.should == "Your order cycle #{oc.name} has been cloned." + + # Then I should have clone of the order cycle + occ = OrderCycle.last + occ.name.should == "COPY OF #{oc.name}" + end end - scenario "editing an order cycle does not affect exchanges we don't manage" do - oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) + context "that is a manager of a participating producer" do + let(:new_user) { create_enterprise_user } - visit edit_admin_order_cycle_path(oc) + before do + new_user.enterprise_roles.build(enterprise: supplier_managed).save + login_to_admin_as new_user + end - # I should not see exchanges for supplier_unmanaged or distributor_unmanaged - page.all('tr.supplier').count.should == 2 - page.all('tr.distributor').count.should == 2 + scenario "editing an order cycle" do + oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: distributor_managed, distributors: [distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) + v1 = create(:variant, product: create(:product, supplier: supplier_managed) ) + v2 = create(:variant, product: create(:product, supplier: supplier_managed) ) - # When I save, then those exchanges should remain - click_button 'Update' - page.should have_content "Your order cycle has been updated." + # Incoming exchange + ex_in = oc.exchanges.where(sender_id: supplier_managed, receiver_id: distributor_managed, incoming: true).first + ex_in.update_attributes(variant_ids: [v1.id, v2.id]) - oc.reload - oc.suppliers.sort.should == [supplier_managed, supplier_permitted, supplier_unmanaged].sort - oc.coordinator.should == supplier_managed - oc.distributors.sort.should == [distributor_managed, distributor_permitted, distributor_unmanaged].sort + # Outgoing exchange + ex_out = oc.exchanges.where(sender_id: distributor_managed, receiver_id: distributor_managed, incoming: false).first + ex_out.update_attributes(variant_ids: [v1.id, v2.id]) + + # Stub editable_variants_for_outgoing_exchanges method so we can test permissions + serializer = Api::Admin::OrderCycleSerializer.new(oc, current_user: new_user) + allow(Api::Admin::OrderCycleSerializer).to receive(:new) { serializer } + allow(serializer).to receive(:editable_variants_for_outgoing_exchanges) do + { "#{distributor_managed.id}" => [v1.id] } + end + + visit edit_admin_order_cycle_path(oc) + + # I should only see exchanges for supplier_managed AND + # distributor_managed and distributor_permitted (who I have given permission to) AND + # and distributor_unmanaged (who distributes my products) + page.all('tr.supplier').count.should == 1 + page.all('tr.distributor').count.should == 2 + + # Open the products list for managed_supplier's incoming exchange + within "tr.distributor-#{distributor_managed.id}" do + page.find("td.products input").click + end + + # I should be able to see and toggle v1 + expect(page).to have_field "order_cycle_outgoing_exchange_0_variants_#{v1.id}", disabled: false + + # I should be able to see but not toggle v2, because I don't have permission + expect(page).to have_field "order_cycle_outgoing_exchange_0_variants_#{v2.id}", disabled: true + + # When I save, any exchanges that I can't manage remain + click_button 'Update' + page.should have_content "Your order cycle has been updated." + + oc.reload + oc.suppliers.sort.should == [supplier_managed, supplier_permitted, supplier_unmanaged].sort + oc.coordinator.should == distributor_managed + oc.distributors.sort.should == [distributor_managed, distributor_permitted, distributor_unmanaged].sort + end end - scenario "editing an order cycle" do - oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: supplier_managed, distributors: [distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) + context "that is the manager of a participating hub" do + let(:my_distributor) { create(:distributor_enterprise) } + let(:new_user) { create_enterprise_user } - visit edit_admin_order_cycle_path(oc) + before do + create(:enterprise_relationship, parent: supplier_managed, child: my_distributor, permissions_list: [:add_to_order_cycle]) - # When I remove all the exchanges and save - page.find("tr.supplier-#{supplier_managed.id} a.remove-exchange").click - page.find("tr.supplier-#{supplier_permitted.id} a.remove-exchange").click - page.find("tr.distributor-#{distributor_managed.id} a.remove-exchange").click - page.find("tr.distributor-#{distributor_permitted.id} a.remove-exchange").click - click_button 'Update' + new_user.enterprise_roles.build(enterprise: my_distributor).save + login_to_admin_as new_user + end - # Then the exchanges should be removed - page.should have_content "Your order cycle has been updated." + scenario "editing an order cycle" do + oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: distributor_managed, distributors: [my_distributor, distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } ) + v1 = create(:variant, product: create(:product, supplier: supplier_managed) ) + v2 = create(:variant, product: create(:product, supplier: supplier_managed) ) + ex = oc.exchanges.where(sender_id: distributor_managed, receiver_id: my_distributor, incoming: false).first + ex.update_attributes(variant_ids: [v1.id, v2.id]) - oc.reload - oc.suppliers.should == [supplier_unmanaged] - oc.coordinator.should == supplier_managed - oc.distributors.should == [distributor_unmanaged] - end + # Stub editable_variants_for_incoming_exchanges method so we can test permissions + serializer = Api::Admin::OrderCycleSerializer.new(oc, current_user: new_user) + allow(Api::Admin::OrderCycleSerializer).to receive(:new) { serializer } + allow(serializer).to receive(:editable_variants_for_incoming_exchanges) do + { "#{supplier_managed.id}" => [v1.id] } + end + visit edit_admin_order_cycle_path(oc) - scenario "cloning an order cycle" do - oc = create(:simple_order_cycle, coordinator: distributor_managed) + # I should see exchanges for my_distributor, and the incoming exchanges supplying the variants in it + page.all('tr.supplier').count.should == 1 + page.all('tr.distributor').count.should == 1 - click_link "Order Cycles" - first('a.clone-order-cycle').click - flash_message.should == "Your order cycle #{oc.name} has been cloned." + # Open the products list for managed_supplier's incoming exchange + within "tr.supplier-#{supplier_managed.id}" do + page.find("td.products input").click + end - # Then I should have clone of the order cycle - occ = OrderCycle.last - occ.name.should == "COPY OF #{oc.name}" + # I should be able to see and toggle v1 + expect(page).to have_field "order_cycle_incoming_exchange_0_variants_#{v1.id}", disabled: false + + # I should be able to see but not toggle v2, because I don't have permission + expect(page).to have_field "order_cycle_incoming_exchange_0_variants_#{v2.id}", disabled: true + + # When I save, any exchange that I can't manage remains + click_button 'Update' + page.should have_content "Your order cycle has been updated." + + oc.reload + oc.suppliers.sort.should == [supplier_managed, supplier_permitted, supplier_unmanaged].sort + oc.coordinator.should == distributor_managed + oc.distributors.sort.should == [my_distributor, distributor_managed, distributor_permitted, distributor_unmanaged].sort + end end end @@ -639,8 +773,8 @@ feature %q{ # And I fill in the basic fields fill_in 'order_cycle_name', with: 'Plums & Avos' - fill_in 'order_cycle_orders_open_at', with: '2014-10-17 06:00:00' - fill_in 'order_cycle_orders_close_at', with: '2014-10-24 17:00:00' + fill_in 'order_cycle_orders_open_at', with: '2040-10-17 06:00:00' + fill_in 'order_cycle_orders_close_at', with: '2040-10-24 17:00:00' fill_in 'order_cycle_outgoing_exchange_0_pickup_time', with: 'pickup time' fill_in 'order_cycle_outgoing_exchange_0_pickup_instructions', with: 'pickup instructions' @@ -665,8 +799,8 @@ feature %q{ # Then my order cycle should have been created page.should have_content 'Your order cycle has been created.' page.should have_selector 'a', text: 'Plums & Avos' - page.should have_selector "input[value='2014-10-17 06:00:00 +1100']" - page.should have_selector "input[value='2014-10-24 17:00:00 +1100']" + page.should have_selector "input[value='2040-10-17 06:00:00 +1100']" + page.should have_selector "input[value='2040-10-24 17:00:00 +1100']" # And it should have some variants selected oc = OrderCycle.last @@ -726,8 +860,8 @@ feature %q{ # And I fill in the basic fields fill_in 'order_cycle_name', with: 'Plums & Avos' - fill_in 'order_cycle_orders_open_at', with: '2014-10-17 06:00:00' - fill_in 'order_cycle_orders_close_at', with: '2014-10-24 17:00:00' + fill_in 'order_cycle_orders_open_at', with: '2040-10-17 06:00:00' + fill_in 'order_cycle_orders_close_at', with: '2040-10-24 17:00:00' fill_in 'order_cycle_outgoing_exchange_0_pickup_time', with: 'xy' fill_in 'order_cycle_outgoing_exchange_0_pickup_instructions', with: 'zzy' @@ -748,8 +882,8 @@ feature %q{ # Then my order cycle should have been updated page.should have_content 'Your order cycle has been updated.' page.should have_selector 'a', text: 'Plums & Avos' - page.should have_selector "input[value='2014-10-17 06:00:00 +1100']" - page.should have_selector "input[value='2014-10-24 17:00:00 +1100']" + page.should have_selector "input[value='2040-10-17 06:00:00 +1100']" + page.should have_selector "input[value='2040-10-24 17:00:00 +1100']" # And it should have a variant selected oc = OrderCycle.last @@ -766,6 +900,15 @@ feature %q{ end end + scenario "deleting an order cycle" do + create(:simple_order_cycle, name: "Translusent Berries") + login_to_admin_section + click_link 'Order Cycles' + page.should have_content("Translusent Berries") + first('a.delete-order-cycle').click + page.should_not have_content("Translusent Berries") + end + private diff --git a/spec/features/admin/products_spec.rb b/spec/features/admin/products_spec.rb index 0d1c3b9c6b..740de12045 100644 --- a/spec/features/admin/products_spec.rb +++ b/spec/features/admin/products_spec.rb @@ -66,7 +66,7 @@ feature %q{ select2_select @enterprise_fees[2].name, :from => 'product_product_distributions_attributes_2_enterprise_fee_id' click_button 'Update' - + product.reload product.distributors.sort.should == [@distributors[0], @distributors[2]].sort @@ -118,6 +118,8 @@ feature %q{ end scenario "creating a new product" do + Spree::Config.products_require_tax_category = false + click_link 'Products' click_link 'New Product' @@ -127,7 +129,7 @@ feature %q{ page.should have_selector('#product_supplier_id') select 'Another Supplier', :from => 'product_supplier_id' select taxon.name, from: "product_primary_taxon_id" - select tax_category.name, from: "product_tax_category_id" + select 'None', from: "product_tax_category_id" # Should only have suppliers listed which the user can manage page.should have_select 'product_supplier_id', with_options: [@supplier2.name, @supplier_permitted.name] @@ -138,7 +140,7 @@ feature %q{ flash_message.should == 'Product "A new product !!!" has been successfully created!' product = Spree::Product.find_by_name('A new product !!!') product.supplier.should == @supplier2 - product.tax_category.should == tax_category + product.tax_category.should be_nil end scenario "editing a product" do @@ -184,16 +186,16 @@ feature %q{ # When I navigate to the product properties page visit spree.admin_product_product_properties_path(p) - page.should have_field 'product_product_properties_attributes_0_property_name', with: 'fooprop', visible: true - page.should have_field 'product_product_properties_attributes_0_value', with: 'fooval', visible: true + page.should have_select2 'product_product_properties_attributes_0_property_name', selected: 'fooprop' + page.should have_field 'product_product_properties_attributes_0_value', with: 'fooval' # And I delete the property page.all('a.remove_fields').first.click wait_until { p.reload.property('fooprop').nil? } # Then the property should have been deleted - page.should_not have_field 'product_product_properties_attributes_0_property_name', with: 'fooprop', visible: true - page.should_not have_field 'product_product_properties_attributes_0_value', with: 'fooval', visible: true + page.should_not have_field 'product_product_properties_attributes_0_property_name', with: 'fooprop' + page.should_not have_field 'product_product_properties_attributes_0_value', with: 'fooval' end @@ -203,13 +205,13 @@ feature %q{ Spree::Image.create({:viewable_id => product.master.id, :viewable_type => 'Spree::Variant', :alt => "position 1", :attachment => image, :position => 1}) visit spree.admin_product_images_path(product) - page.should have_selector "table[data-hook='images_table'] td img", visible: true + page.should have_selector "table[data-hook='images_table'] td img" product.reload.images.count.should == 1 page.find('a.delete-resource').click wait_until { product.reload.images.count == 0 } - page.should_not have_selector "table[data-hook='images_table'] td img", visible: true + page.should_not have_selector "table[data-hook='images_table'] td img" end end end diff --git a/spec/features/admin/reports_spec.rb b/spec/features/admin/reports_spec.rb index 0524a6769e..f16b736c00 100644 --- a/spec/features/admin/reports_spec.rb +++ b/spec/features/admin/reports_spec.rb @@ -65,12 +65,21 @@ feature %q{ click_link "Reports" end - scenario "order payment method report" do - click_link "Order Cycle Management" + scenario "payment method report" do + click_link "Payment Methods Report" rows = find("table#listing_order_payment_methods").all("thead tr") table = rows.map { |r| r.all("th").map { |c| c.text.strip } } table.sort.should == [ - ["First Name", "Last Name", "Email", "Phone", "Hub", "Shipping Method", "Payment Method", "Amount"] + ["First Name", "Last Name", "Hub", "Hub Code", "Email", "Phone", "Shipping Method", "Payment Method", "Amount", "Balance"] + ].sort + end + + scenario "delivery report" do + click_link "Delivery Report" + rows = find("table#listing_order_payment_methods").all("thead tr") + table = rows.map { |r| r.all("th").map { |c| c.text.strip } } + table.sort.should == [ + ["First Name", "Last Name", "Hub", "Hub Code", "Delivery Address", "Delivery Postcode", "Phone", "Shipping Method", "Payment Method", "Amount", "Balance", "Temp Controlled Items?", "Special Instructions"] ].sort end end @@ -98,6 +107,66 @@ feature %q{ page.should have_content 'Payment State' end + + describe "Sales tax report" do + let(:distributor1) { create(:distributor_enterprise, with_payment_and_shipping: true) } + let(:distributor2) { create(:distributor_enterprise, with_payment_and_shipping: true) } + let(:user1) { create_enterprise_user enterprises: [distributor1] } + let(:user2) { create_enterprise_user enterprises: [distributor2] } + let(:shipping_method) { create(:shipping_method, name: "Shipping", description: "Expensive", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 100.55)) } + let(:enterprise_fee) { create(:enterprise_fee, enterprise: user1.enterprises.first, tax_category: product2.tax_category, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 120.0)) } + let(:order_cycle) { create(:simple_order_cycle, coordinator: distributor1, coordinator_fees: [enterprise_fee], distributors: [distributor1], variants: [product1.master]) } + + let!(:zone) { create(:zone_with_member) } + let(:order1) { create(:order, order_cycle: order_cycle, distributor: user1.enterprises.first, shipping_method: shipping_method, bill_address: create(:address)) } + let(:product1) { create(:taxed_product, zone: zone, price: 12.54, tax_rate_amount: 0) } + let(:product2) { create(:taxed_product, zone: zone, price: 500.15, tax_rate_amount: 0.2) } + + let!(:line_item1) { create(:line_item, variant: product1.master, price: 12.54, quantity: 1, order: order1) } + let!(:line_item2) { create(:line_item, variant: product2.master, price: 500.15, quantity: 3, order: order1) } + + let!(:adj_shipping) { create(:adjustment, adjustable: order1, label: "Shipping", amount: 100.55) } + + before do + Spree::Config.shipment_inc_vat = true + Spree::Config.shipping_tax_rate = 0.2 + + 3.times { order1.next } + order1.reload.update_distribution_charge! + + order1.finalize! + + login_to_admin_as user1 + click_link "Reports" + click_link "Sales Tax" + end + + it "reports" do + # Then it should give me access only to managed enterprises + page.should have_select 'q_distributor_id_eq', with_options: [user1.enterprises.first.name] + page.should_not have_select 'q_distributor_id_eq', with_options: [user2.enterprises.first.name] + + # When I filter to just one distributor + select user1.enterprises.first.name, from: 'q_distributor_id_eq' + click_button 'Search' + + # Then I should see the relevant order + page.should have_content "#{order1.number}" + + # And the totals and sales tax should be correct + page.should have_content "1512.99" # items total + page.should have_content "1500.45" # taxable items total + page.should have_content "250.08" # sales tax + page.should have_content "20.0" # enterprise fee tax + + # And the shipping cost and tax should be correct + page.should have_content "100.55" # shipping cost + page.should have_content "16.76" # shipping tax + + # And the total tax should be correct + page.should have_content "286.84" # total tax + end + end describe "orders & fulfilment reports" do it "loads the report page" do diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index 886f7d7e5b..ee615f44f5 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -11,6 +11,7 @@ feature %q{ let!(:hub) { create(:distributor_enterprise) } let!(:hub2) { create(:distributor_enterprise) } + let!(:hub3) { create(:distributor_enterprise) } let!(:producer) { create(:supplier_enterprise) } let!(:er1) { create(:enterprise_relationship, parent: hub, child: producer, permissions_list: [:add_to_order_cycle]) } @@ -156,6 +157,7 @@ feature %q{ context "with overrides" do let!(:vo) { create(:variant_override, variant: variant, hub: hub, price: 77.77, count_on_hand: 11111) } + let!(:vo_no_auth) { create(:variant_override, variant: variant, hub: hub3, price: 1, count_on_hand: 2) } before do visit '/admin/variant_overrides' diff --git a/spec/features/consumer/authentication_spec.rb b/spec/features/consumer/authentication_spec.rb index f8b405374e..e3fa340909 100644 --- a/spec/features/consumer/authentication_spec.rb +++ b/spec/features/consumer/authentication_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' feature "Authentication", js: true do include UIComponentHelper + describe "login" do let(:user) { create(:user, password: "password", password_confirmation: "password") } @@ -32,7 +33,7 @@ feature "Authentication", js: true do scenario "failing to login" do fill_in "Email", with: user.email click_login_button - page.should have_content "Invalid email or password" + page.should have_content "Invalid email or password" end scenario "logging in successfully" do @@ -44,9 +45,9 @@ feature "Authentication", js: true do describe "signing up" do before do - ActionMailer::Base.deliveries.clear select_login_tab "Sign up" end + scenario "Failing to sign up because password is too short" do fill_in "Email", with: "test@foo.com" fill_in "Choose a password", with: "short" @@ -58,10 +59,11 @@ feature "Authentication", js: true do fill_in "Email", with: "test@foo.com" fill_in "Choose a password", with: "test12345" fill_in "Confirm password", with: "test12345" - click_signup_button - page.should have_content "Welcome! You have signed up successfully" + expect do + click_signup_button + page.should have_content "Welcome! You have signed up successfully" + end.to enqueue_job ConfirmSignupJob page.should be_logged_in_as "test@foo.com" - ActionMailer::Base.deliveries.last.subject.should =~ /Welcome to/ end end @@ -70,7 +72,7 @@ feature "Authentication", js: true do ActionMailer::Base.deliveries.clear select_login_tab "Forgot Password?" end - + scenario "failing to reset password" do fill_in "Your email", with: "notanemail@myemail.com" click_reset_password_button @@ -78,7 +80,7 @@ feature "Authentication", js: true do end scenario "resetting password" do - fill_in "Your email", with: user.email + fill_in "Your email", with: user.email click_reset_password_button page.should have_reset_password ActionMailer::Base.deliveries.last.subject.should =~ /Password Reset/ @@ -90,30 +92,17 @@ feature "Authentication", js: true do browse_as_medium end scenario "showing login" do - open_off_canvas + open_off_canvas open_login_modal page.should have_login_modal end end end - describe "oldskool" do - 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 + scenario "Loggin by typing login/ redirects to /#/login" do + visit "/login" + uri = URI.parse(current_url) + (uri.path + "#" + uri.fragment).should == '/#/login' end end end - diff --git a/spec/features/consumer/groups_spec.rb b/spec/features/consumer/groups_spec.rb index b2db10d58c..e51ac39ea2 100644 --- a/spec/features/consumer/groups_spec.rb +++ b/spec/features/consumer/groups_spec.rb @@ -14,9 +14,6 @@ feature 'Groups', js: true do it "renders enterprise modals for groups" do visit groups_path - page.should have_content enterprise.name - open_enterprise_modal enterprise - modal_should_be_open_for enterprise - page.should have_content "Herndon, Vic" + page.should have_content group.name end end diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index d46f0ae2f1..12eca09339 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -16,7 +16,6 @@ feature "As a consumer I want to check out my cart", js: true do let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } before do - ActionMailer::Base.deliveries.clear add_enterprise_fee enterprise_fee set_order order add_product_to_cart @@ -80,7 +79,6 @@ feature "As a consumer I want to check out my cart", js: true do context "on the checkout page with payments open" do before do - ActionMailer::Base.deliveries.clear visit checkout_path checkout_as_guest toggle_payment @@ -112,7 +110,7 @@ feature "As a consumer I want to check out my cart", js: true do toggle_shipping within "#shipping" do choose sm2.name - fill_in 'Any notes or custom delivery instructions?', with: "SpEcIaL NoTeS" + fill_in 'Any comments or special instructions?', with: "SpEcIaL NoTeS" end toggle_payment within "#payment" do @@ -120,13 +118,11 @@ feature "As a consumer I want to check out my cart", js: true do end - ActionMailer::Base.deliveries.length.should == 0 - place_order - page.should have_content "Your order has been processed successfully" - ActionMailer::Base.deliveries.length.should == 2 - email = ActionMailer::Base.deliveries.last - site_name = Spree::Config[:site_name] - email.subject.should include "#{site_name} Order Confirmation" + expect do + place_order + page.should have_content "Your order has been processed successfully" + end.to enqueue_job ConfirmOrderJob + o = Spree::Order.complete.first expect(o.special_instructions).to eq "SpEcIaL NoTeS" end @@ -214,7 +210,6 @@ feature "As a consumer I want to check out my cart", js: true do context "when the customer has a pre-set shipping and billing address" do before do - ActionMailer::Base.deliveries.clear # Load up the customer's order and give them a shipping and billing address # This is equivalent to when the customer has ordered before and their addresses # are pre-populated. @@ -231,10 +226,10 @@ feature "As a consumer I want to check out my cart", js: true do toggle_payment choose pm1.name - expect(ActionMailer::Base.deliveries.length).to be 0 - place_order - page.should have_content "Your order has been processed successfully" - expect(ActionMailer::Base.deliveries.length).to be 2 + expect do + place_order + page.should have_content "Your order has been processed successfully" + end.to enqueue_job ConfirmOrderJob end end end diff --git a/spec/features/consumer/shopping/variant_overrides_spec.rb b/spec/features/consumer/shopping/variant_overrides_spec.rb index 4178aeb2cc..98d58e174f 100644 --- a/spec/features/consumer/shopping/variant_overrides_spec.rb +++ b/spec/features/consumer/shopping/variant_overrides_spec.rb @@ -26,9 +26,9 @@ feature "shopping with variant overrides defined", js: true do let(:ef) { create(:enterprise_fee, enterprise: hub, fee_type: 'packing', calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 10)) } before do - ActionMailer::Base.deliveries.clear outgoing_exchange.variants = [v1, v2, v3, v4] outgoing_exchange.enterprise_fees << ef + sm.calculator.preferred_amount = 0 visit shop_path click_link hub.name end @@ -109,15 +109,9 @@ feature "shopping with variant overrides defined", js: true do complete_checkout - ActionMailer::Base.deliveries.length.should == 2 - email = ActionMailer::Base.deliveries.last - o = Spree::Order.complete.last - o.line_items.first.price.should == 55.55 o.total.should == 122.21 - - email.body.should include "$122.21" end it "subtracts stock from the override" do diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 08a4335bae..69e7a3add4 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -1,15 +1,17 @@ require 'spec_helper' -# Specs in this file have access to a helper object that includes -# the GroupsHelper. For example: -# -# describe GroupsHelper do -# describe "string concat" do -# it "concats two strings with spaces" do -# expect(helper.concat_strings("this","that")).to eq("this that") -# end -# end -# end describe GroupsHelper do - pending "add some examples to (or delete) #{__FILE__}" + describe "ext_url" do + it "adds prefix if missing" do + expect(helper.ext_url("http://example.com/", "http://example.com/bla")).to eq("http://example.com/bla") + expect(helper.ext_url("http://example.com/", "bla")).to eq("http://example.com/bla") + end + end + describe "strip_url" do + it "removes http(s)://" do + expect(helper.strip_url("http://example.com/")).to eq("example.com/") + expect(helper.strip_url("https://example.com/")).to eq("example.com/") + expect(helper.strip_url("example.com")).to eq("example.com") + end + end end diff --git a/spec/helpers/order_cycles_helper_spec.rb b/spec/helpers/order_cycles_helper_spec.rb index c687463d59..6841889313 100644 --- a/spec/helpers/order_cycles_helper_spec.rb +++ b/spec/helpers/order_cycles_helper_spec.rb @@ -1,31 +1,74 @@ require 'spec_helper' describe OrderCyclesHelper do - describe "finding hub enterprises" do - let(:e) { create(:distributor_enterprise, name: 'enterprise') } + let(:oc) { double(:order_cycle) } + describe "finding producer enterprise options" do before do - helper.stub(:order_cycle_permitted_enterprises) { Enterprise.where(id: e.id) } + helper.stub(:permitted_producer_enterprises_for) { "enterprise list" } end + it "asks for a validation option list" do + expect(helper).to receive(:validated_enterprise_options).with("enterprise list", {confirmed: true}) + helper.permitted_producer_enterprise_options_for(oc) + end + end + + describe "finding coodinator enterprise options" do + before do + helper.stub(:permitted_coordinating_enterprises_for) { "enterprise list" } + end + + it "asks for a validation option list" do + expect(helper).to receive(:validated_enterprise_options).with("enterprise list", {confirmed: true}) + helper.permitted_coordinating_enterprise_options_for(oc) + end + end + + describe "finding hub enterprise options" do + before do + helper.stub(:permitted_hub_enterprises_for) { "enterprise list" } + end + + it "asks for a validation option list" do + expect(helper).to receive(:validated_enterprise_options).with("enterprise list", {confirmed: true, shipping_and_payment_methods: true}) + helper.permitted_hub_enterprise_options_for(oc) + end + end + + describe "building a validated enterprise list" do + let(:e) { create(:distributor_enterprise, name: 'enterprise') } + it "returns enterprises without shipping methods as disabled" do create(:payment_method, distributors: [e]) - helper.order_cycle_hub_enterprises.should == [['enterprise (no shipping methods)', e.id, {disabled: true}]] + expect(helper.send(:validated_enterprise_options, [e], shipping_and_payment_methods: true)) + .to eq [['enterprise (no shipping methods)', e.id, {disabled: true}]] end it "returns enterprises without payment methods as disabled" do create(:shipping_method, distributors: [e]) - helper.order_cycle_hub_enterprises.should == [['enterprise (no payment methods)', e.id, {disabled: true}]] + expect(helper.send(:validated_enterprise_options, [e], shipping_and_payment_methods: true)) + .to eq [['enterprise (no payment methods)', e.id, {disabled: true}]] end it "returns enterprises with unavailable payment methods as disabled" do create(:shipping_method, distributors: [e]) create(:payment_method, distributors: [e], active: false) - helper.order_cycle_hub_enterprises.should == [['enterprise (no payment methods)', e.id, {disabled: true}]] + expect(helper.send(:validated_enterprise_options, [e], shipping_and_payment_methods: true)) + .to eq [['enterprise (no payment methods)', e.id, {disabled: true}]] + end + + it "returns unconfirmed enterprises as disabled" do + create(:shipping_method, distributors: [e]) + create(:payment_method, distributors: [e]) + e.stub(:confirmed_at) { nil } + expect(helper.send(:validated_enterprise_options, [e], confirmed: true)) + .to eq [['enterprise (unconfirmed)', e.id, {disabled: true}]] end it "returns enterprises with neither shipping nor payment methods as disabled" do - helper.order_cycle_hub_enterprises.should == [['enterprise (no shipping or payment methods)', e.id, {disabled: true}]] + expect(helper.send(:validated_enterprise_options, [e], shipping_and_payment_methods: true)) + .to eq [['enterprise (no shipping or payment methods)', e.id, {disabled: true}]] end end @@ -33,8 +76,8 @@ describe OrderCyclesHelper do it "gives me the pickup time for the current order cycle" do d = create(:distributor_enterprise, name: 'Green Grass') oc1 = create(:simple_order_cycle, name: 'oc 1', distributors: [d]) - exchange = Exchange.find(oc1.exchanges.to_enterprises(d).outgoing.first.id) - exchange.update_attribute :pickup_time, "turtles" + 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 @@ -46,8 +89,8 @@ describe OrderCyclesHelper do oc1 = create(:simple_order_cycle, name: 'oc 1', distributors: [d]) oc2= create(:simple_order_cycle, name: 'oc 1', distributors: [d]) - exchange = Exchange.find(oc2.exchanges.to_enterprises(d).outgoing.first.id) - exchange.update_attribute :pickup_time, "turtles" + 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 diff --git a/spec/javascripts/unit/admin/order_cycles/controllers/simple_create.js.coffee b/spec/javascripts/unit/admin/order_cycles/controllers/simple_create.js.coffee index bf2498e234..0f1e043467 100644 --- a/spec/javascripts/unit/admin/order_cycles/controllers/simple_create.js.coffee +++ b/spec/javascripts/unit/admin/order_cycles/controllers/simple_create.js.coffee @@ -9,22 +9,27 @@ describe "AdminSimpleCreateOrderCycleCtrl", -> beforeEach -> scope = {} + order_cycle = + coordinator_id: 123 + incoming_exchanges: [incoming_exchange] + outgoing_exchanges: [outgoing_exchange] OrderCycle = - order_cycle: - incoming_exchanges: [incoming_exchange] - outgoing_exchanges: [outgoing_exchange] + order_cycle: order_cycle addSupplier: jasmine.createSpy() addDistributor: jasmine.createSpy() setExchangeVariants: jasmine.createSpy() + new: jasmine.createSpy().andReturn order_cycle Enterprise = + get: jasmine.createSpy().andReturn {id: 123} index: jasmine.createSpy() suppliedVariants: jasmine.createSpy().andReturn('supplied variants') EnterpriseFee = index: jasmine.createSpy() + ocInstance = {} module('admin.order_cycles') inject ($controller) -> - ctrl = $controller 'AdminSimpleCreateOrderCycleCtrl', {$scope: scope, OrderCycle: OrderCycle, Enterprise: Enterprise, EnterpriseFee: EnterpriseFee} + ctrl = $controller 'AdminSimpleCreateOrderCycleCtrl', {$scope: scope, OrderCycle: OrderCycle, Enterprise: Enterprise, EnterpriseFee: EnterpriseFee, ocInstance: ocInstance} describe "initialisation", -> enterprise = {id: 123} @@ -46,4 +51,4 @@ describe "AdminSimpleCreateOrderCycleCtrl", -> toHaveBeenCalledWith(incoming_exchange, 'supplied variants', true) it "sets the coordinator", -> - expect(OrderCycle.order_cycle.coordinator_id).toEqual enterprise.id \ No newline at end of file + expect(OrderCycle.order_cycle.coordinator_id).toEqual enterprise.id diff --git a/spec/javascripts/unit/darkswarm/filters/ext_url_spec.js.coffee b/spec/javascripts/unit/darkswarm/filters/ext_url_spec.js.coffee new file mode 100644 index 0000000000..1575d8c246 --- /dev/null +++ b/spec/javascripts/unit/darkswarm/filters/ext_url_spec.js.coffee @@ -0,0 +1,19 @@ +describe "ensuring absolute URL", -> + filter = null + + beforeEach -> + module 'Darkswarm' + inject ($filter) -> + filter = $filter 'ext_url' + + it "returns null when no URL given", -> + expect(filter(null, "http://")).toBeNull() + + it "returns the URL as-is for http URLs", -> + expect(filter("http://example.com", "http://")).toEqual "http://example.com" + + it "returns the URL as-is for https URLs", -> + expect(filter("https://example.com", "https://")).toEqual "https://example.com" + + it "returns with URL with prefix added when a relative URL is given", -> + expect(filter("example.com", "http://")).toEqual "http://example.com" diff --git a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee index 4d13ccc8b5..4be1a13dd8 100644 --- a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee @@ -6,7 +6,10 @@ describe 'Cart service', -> beforeEach -> module 'Darkswarm' - variant = {id: 1} + variant = + id: 1 + name_to_display: 'name' + product_name: 'name' order = { line_items: [ variant: variant @@ -23,6 +26,9 @@ describe 'Cart service', -> it "registers variants with the Variants service", -> expect(Variants.variants[1]).toBe variant + it "generates extended variant names", -> + expect(Cart.line_items[0].variant.extended_name).toEqual "name" + it "creates and backreferences new line items if necessary", -> Cart.register_variant(v2 = {id: 2}) expect(Cart.line_items[1].variant).toBe v2 @@ -37,3 +43,23 @@ describe 'Cart service', -> expect(Cart.line_items_present()).toEqual [] order.line_items[0].quantity = 2 expect(Cart.total_item_count()).toEqual 2 + + describe "generating an extended variant name", -> + it "returns the product name when it is the same as the variant name", -> + variant = {product_name: 'product_name', name_to_display: 'product_name'} + expect(Cart.extendedVariantName(variant)).toEqual "product_name" + + describe "when the product name and the variant name differ", -> + it "returns a combined name when there is no options text", -> + variant = + product_name: 'product_name' + name_to_display: 'name_to_display' + expect(Cart.extendedVariantName(variant)).toEqual "product_name - name_to_display" + + it "returns a combined name when there is some options text", -> + variant = + product_name: 'product_name' + name_to_display: 'name_to_display' + options_text: 'options_text' + + expect(Cart.extendedVariantName(variant)).toEqual "product_name - name_to_display (options_text)" diff --git a/spec/javascripts/unit/order_cycle_spec.js.coffee b/spec/javascripts/unit/order_cycle_spec.js.coffee index 6a1e1be70f..c14de6742d 100644 --- a/spec/javascripts/unit/order_cycle_spec.js.coffee +++ b/spec/javascripts/unit/order_cycle_spec.js.coffee @@ -13,7 +13,6 @@ describe 'OrderCycle controllers', -> event = preventDefault: jasmine.createSpy('preventDefault') OrderCycle = - order_cycle: 'my order cycle' exchangeSelectedVariants: jasmine.createSpy('exchangeSelectedVariants').andReturn('variants selected') productSuppliedToOrderCycle: jasmine.createSpy('productSuppliedToOrderCycle').andReturn('product supplied') variantSuppliedToOrderCycle: jasmine.createSpy('variantSuppliedToOrderCycle').andReturn('variant supplied') @@ -29,6 +28,7 @@ describe 'OrderCycle controllers', -> removeExchangeFee: jasmine.createSpy('removeExchangeFee') removeDistributionOfVariant: jasmine.createSpy('removeDistributionOfVariant') create: jasmine.createSpy('create') + new: jasmine.createSpy('new').andReturn "my order cycle" Enterprise = index: jasmine.createSpy('index').andReturn('enterprises list') supplied_products: 'supplied products' @@ -37,10 +37,11 @@ describe 'OrderCycle controllers', -> EnterpriseFee = index: jasmine.createSpy('index').andReturn('enterprise fees list') forEnterprise: jasmine.createSpy('forEnterprise').andReturn('enterprise fees for enterprise') + ocInstance = {} module('admin.order_cycles') inject ($controller) -> - ctrl = $controller 'AdminCreateOrderCycleCtrl', {$scope: scope, OrderCycle: OrderCycle, Enterprise: Enterprise, EnterpriseFee: EnterpriseFee} + ctrl = $controller 'AdminCreateOrderCycleCtrl', {$scope: scope, OrderCycle: OrderCycle, Enterprise: Enterprise, EnterpriseFee: EnterpriseFee, ocInstance: ocInstance} it 'Loads enterprises and supplied products', -> @@ -93,12 +94,14 @@ describe 'OrderCycle controllers', -> expect(scope.exchangeDirection('exchange')).toEqual('exchange direction') expect(OrderCycle.exchangeDirection).toHaveBeenCalledWith('exchange') - it 'Finds enterprises participating in the order cycle', -> + it 'Finds enterprises participating in the order cycle that have fees', -> scope.enterprises = 1: {id: 1, name: 'Eaterprises'} 2: {id: 2, name: 'Pepper Tree Place'} + 3: {id: 3, name: 'South East'} OrderCycle.participatingEnterpriseIds = jasmine.createSpy('participatingEnterpriseIds').andReturn([2]) - expect(scope.participatingEnterprises()).toEqual([ + EnterpriseFee.enterprise_fees = [ {enterprise_id: 2} ] # Pepper Tree Place has a fee + expect(scope.enterprisesWithFees()).toEqual([ {id: 2, name: 'Pepper Tree Place'} ]) @@ -254,12 +257,14 @@ describe 'OrderCycle controllers', -> expect(scope.exchangeDirection('exchange')).toEqual('exchange direction') expect(OrderCycle.exchangeDirection).toHaveBeenCalledWith('exchange') - it 'Finds enterprises participating in the order cycle', -> + it 'Finds enterprises participating in the order cycle that have fees', -> scope.enterprises = 1: {id: 1, name: 'Eaterprises'} 2: {id: 2, name: 'Pepper Tree Place'} + 3: {id: 3, name: 'South East'} OrderCycle.participatingEnterpriseIds = jasmine.createSpy('participatingEnterpriseIds').andReturn([2]) - expect(scope.participatingEnterprises()).toEqual([ + EnterpriseFee.enterprise_fees = [ {enterprise_id: 2} ] # Pepper Tree Place has a fee + expect(scope.enterprisesWithFees()).toEqual([ {id: 2, name: 'Pepper Tree Place'} ]) @@ -329,7 +334,7 @@ describe 'OrderCycle services', -> inject ($injector, _$httpBackend_)-> Enterprise = $injector.get('Enterprise') $httpBackend = _$httpBackend_ - $httpBackend.whenGET('/admin/enterprises/for_order_cycle.json').respond [ + $httpBackend.whenGET('/admin/enterprises/for_order_cycle.json?').respond [ {id: 1, name: 'One', supplied_products: [1, 2]} {id: 2, name: 'Two', supplied_products: [3, 4]} {id: 3, name: 'Three', supplied_products: [5, 6]} @@ -395,7 +400,7 @@ describe 'OrderCycle services', -> inject ($injector, _$httpBackend_)-> EnterpriseFee = $injector.get('EnterpriseFee') $httpBackend = _$httpBackend_ - $httpBackend.whenGET('/admin/enterprise_fees.json').respond [ + $httpBackend.whenGET('/admin/enterprise_fees/for_order_cycle.json?').respond [ {id: 1, name: "Yayfee", enterprise_id: 1} {id: 2, name: "FeeTwo", enterprise_id: 2} ] @@ -449,12 +454,15 @@ describe 'OrderCycle services', -> {sender_id: 1, receiver_id: 456, incoming: true} {sender_id: 456, receiver_id: 2, incoming: false} ] + $httpBackend.whenGET('/admin/order_cycles/new.json').respond + id: 123 + name: 'New Order Cycle' + coordinator_id: 456 + coordinator_fees: [] + exchanges: [] it 'initialises order cycle', -> - expect(OrderCycle.order_cycle).toEqual - incoming_exchanges: [] - outgoing_exchanges: [] - coordinator_fees: [] + expect(OrderCycle.order_cycle).toEqual {} it 'counts selected variants in an exchange', -> result = OrderCycle.exchangeSelectedVariants({variants: {1: true, 2: false, 3: true}}) @@ -494,14 +502,32 @@ describe 'OrderCycle services', -> expect(exchange.showProducts).toEqual(true) describe "setting exchange variants", -> - it "sets all variants to the provided value", -> - exchange = {variants: {2: false}} - OrderCycle.setExchangeVariants(exchange, [1, 2, 3], true) - expect(exchange.variants).toEqual {1: true, 2: true, 3: true} + describe "when I have permissions to edit the variants", -> + beforeEach -> + OrderCycle.order_cycle["editable_variants_for_outgoing_exchanges"] = { 1: [1, 2, 3] } + + it "sets all variants to the provided value", -> + exchange = { enterprise_id: 1, incoming: false, variants: {2: false}} + OrderCycle.setExchangeVariants(exchange, [1, 2, 3], true) + expect(exchange.variants).toEqual {1: true, 2: true, 3: true} + + describe "when I don't have permissions to edit the variants", -> + beforeEach -> + OrderCycle.order_cycle["editable_variants_for_outgoing_exchanges"] = { 1: [] } + + it "does not change variants to the provided value", -> + exchange = { enterprise_id: 1, incoming: false, variants: {2: false}} + OrderCycle.setExchangeVariants(exchange, [1, 2, 3], true) + expect(exchange.variants).toEqual {2: false} describe 'adding suppliers', -> exchange = null + beforeEach -> + # Initialise OC + OrderCycle.new() + $httpBackend.flush() + it 'adds the supplier to incoming exchanges', -> OrderCycle.addSupplier('123') expect(OrderCycle.order_cycle.incoming_exchanges).toEqual [ @@ -511,6 +537,11 @@ describe 'OrderCycle services', -> describe 'adding distributors', -> exchange = null + beforeEach -> + # Initialise OC + OrderCycle.new() + $httpBackend.flush() + it 'adds the distributor to outgoing exchanges', -> OrderCycle.addDistributor('123') expect(OrderCycle.order_cycle.outgoing_exchanges).toEqual [ @@ -558,6 +589,9 @@ describe 'OrderCycle services', -> expect(OrderCycle.removeDistributionOfVariant).not.toHaveBeenCalled() it 'adds coordinator fees', -> + # Initialise OC + OrderCycle.new() + $httpBackend.flush() OrderCycle.addCoordinatorFee() expect(OrderCycle.order_cycle.coordinator_fees).toEqual [{}] @@ -679,7 +713,25 @@ describe 'OrderCycle services', -> $httpBackend.flush() expect(OrderCycle.loaded).toBe(true) - describe 'loading an order cycle', -> + describe 'loading a new order cycle', -> + beforeEach -> + OrderCycle.new() + $httpBackend.flush() + + + it 'loads basic fields', -> + expect(OrderCycle.order_cycle.id).toEqual(123) + expect(OrderCycle.order_cycle.name).toEqual('New Order Cycle') + expect(OrderCycle.order_cycle.coordinator_id).toEqual(456) + + it 'initialises the incoming and outgoing exchanges', -> + expect(OrderCycle.order_cycle.incoming_exchanges).toEqual [] + expect(OrderCycle.order_cycle.outgoing_exchanges).toEqual [] + + it 'removes the original exchanges array', -> + expect(OrderCycle.order_cycle.exchanges).toBeUndefined() + + describe 'loading an existing order cycle', -> beforeEach -> OrderCycle.load('123') $httpBackend.flush() @@ -704,8 +756,8 @@ describe 'OrderCycle services', -> active: true ] - it 'removes original exchanges array', -> - expect(OrderCycle.order_cycle.exchanges).toEqual(undefined) + it 'removes the original exchanges array', -> + expect(OrderCycle.order_cycle.exchanges).toBeUndefined() describe 'creating an order cycle', -> it 'redirects to the order cycles page on success', -> diff --git a/spec/jobs/confirm_order_job_spec.rb b/spec/jobs/confirm_order_job_spec.rb new file mode 100644 index 0000000000..e3138620e3 --- /dev/null +++ b/spec/jobs/confirm_order_job_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe ConfirmOrderJob do + let(:order) { create(:order) } + + it "sends confirmation emails to both the user and the shop owner" do + customer_confirm_fake = double(:confirm_email_for_customer) + shop_confirm_fake = double(:confirm_email_for_shop) + expect(Spree::OrderMailer).to receive(:confirm_email_for_customer).and_return customer_confirm_fake + expect(Spree::OrderMailer).to receive(:confirm_email_for_shop).and_return shop_confirm_fake + expect(customer_confirm_fake).to receive :deliver + expect(shop_confirm_fake).to receive :deliver + + run_job ConfirmOrderJob.new order.id + end +end diff --git a/spec/jobs/confirm_signup_job_spec.rb b/spec/jobs/confirm_signup_job_spec.rb new file mode 100644 index 0000000000..4d7b97bcc0 --- /dev/null +++ b/spec/jobs/confirm_signup_job_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe ConfirmSignupJob do + let(:user) { create(:user) } + + it "sends a confirmation email to the user" do + mail = double(:mail) + Spree::UserMailer.should_receive(:signup_confirmation).with(user).and_return(mail) + mail.should_receive(:deliver) + + run_job ConfirmSignupJob.new user.id + end +end diff --git a/spec/jobs/welcome_enterprise_job_spec.rb b/spec/jobs/welcome_enterprise_job_spec.rb new file mode 100644 index 0000000000..051f4a31d1 --- /dev/null +++ b/spec/jobs/welcome_enterprise_job_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe WelcomeEnterpriseJob do + let(:enterprise) { create(:enterprise) } + + it "sends a welcome email to the enterprise" do + mail = double(:mail) + EnterpriseMailer.should_receive(:welcome).with(enterprise).and_return(mail) + mail.should_receive(:deliver) + + run_job WelcomeEnterpriseJob.new(enterprise.id) + end +end diff --git a/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb b/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb index 9ad1d222b3..6703f844ab 100644 --- a/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb +++ b/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb @@ -65,4 +65,33 @@ module OpenFoodNetwork efa.send(:order_adjustment_label).should == "Whole order - packing fee by distributor Ballantyne" end end + + describe "ensuring that tax rate is marked as tax included_in_price" do + let(:efa) { EnterpriseFeeApplicator.new nil, nil, nil } + let(:tax_rate) { create(:tax_rate, included_in_price: false, calculator: Spree::Calculator::DefaultTax.new) } + + it "sets included_in_price to true" do + efa.send(:with_tax_included_in_price, tax_rate) do + tax_rate.included_in_price.should be_true + end + end + + it "sets the included_in_price value accessible to the calculator to true" do + efa.send(:with_tax_included_in_price, tax_rate) do + tax_rate.calculator.calculable.included_in_price.should be_true + end + end + + it "passes through the return value of the block" do + efa.send(:with_tax_included_in_price, tax_rate) do + 'asdf' + end.should == 'asdf' + end + + it "restores both values to their original afterwards" do + efa.send(:with_tax_included_in_price, tax_rate) {} + tax_rate.included_in_price.should be_false + tax_rate.calculator.calculable.included_in_price.should be_false + end + end end diff --git a/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb b/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb index c63d1eac25..6a03964039 100644 --- a/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb +++ b/spec/lib/open_food_network/order_cycle_form_applicator_spec.rb @@ -2,6 +2,10 @@ require 'open_food_network/order_cycle_form_applicator' module OpenFoodNetwork describe OrderCycleFormApplicator do + include AuthenticationWorkflow + + let!(:user) { create_enterprise_user } + context "unit specs" do it "creates new exchanges for incoming_exchanges" do coordinator_id = 123 @@ -11,9 +15,9 @@ module OpenFoodNetwork oc = double(:order_cycle, :coordinator_id => coordinator_id, :exchanges => [], :incoming_exchanges => [incoming_exchange], :outgoing_exchanges => []) - applicator = OrderCycleFormApplicator.new(oc, []) + applicator = OrderCycleFormApplicator.new(oc, user) - applicator.should_receive(:exchange_variant_ids).with(incoming_exchange).and_return([1, 3]) + applicator.should_receive(:incoming_exchange_variant_ids).with(incoming_exchange).and_return([1, 3]) applicator.should_receive(:exchange_exists?).with(supplier_id, coordinator_id, true).and_return(false) applicator.should_receive(:add_exchange).with(supplier_id, coordinator_id, true, {:variant_ids => [1, 3], :enterprise_fee_ids => [1, 2]}) applicator.should_receive(:destroy_untouched_exchanges) @@ -29,9 +33,9 @@ module OpenFoodNetwork oc = double(:order_cycle, :coordinator_id => coordinator_id, :exchanges => [], :incoming_exchanges => [], :outgoing_exchanges => [outgoing_exchange]) - applicator = OrderCycleFormApplicator.new(oc, []) + applicator = OrderCycleFormApplicator.new(oc, user) - applicator.should_receive(:exchange_variant_ids).with(outgoing_exchange).and_return([1, 3]) + applicator.should_receive(:outgoing_exchange_variant_ids).with(outgoing_exchange).and_return([1, 3]) applicator.should_receive(:exchange_exists?).with(coordinator_id, distributor_id, false).and_return(false) applicator.should_receive(:add_exchange).with(coordinator_id, distributor_id, false, {:variant_ids => [1, 3], :enterprise_fee_ids => [1, 2], :pickup_time => 'pickup time', :pickup_instructions => 'pickup instructions'}) applicator.should_receive(:destroy_untouched_exchanges) @@ -51,9 +55,9 @@ module OpenFoodNetwork :incoming_exchanges => [incoming_exchange], :outgoing_exchanges => []) - applicator = OrderCycleFormApplicator.new(oc, []) + applicator = OrderCycleFormApplicator.new(oc, user) - applicator.should_receive(:exchange_variant_ids).with(incoming_exchange).and_return([1, 3]) + applicator.should_receive(:incoming_exchange_variant_ids).with(incoming_exchange).and_return([1, 3]) applicator.should_receive(:exchange_exists?).with(supplier_id, coordinator_id, true).and_return(true) applicator.should_receive(:update_exchange).with(supplier_id, coordinator_id, true, {:variant_ids => [1, 3], :enterprise_fee_ids => [1, 2]}) applicator.should_receive(:destroy_untouched_exchanges) @@ -73,9 +77,9 @@ module OpenFoodNetwork :incoming_exchanges => [], :outgoing_exchanges => [outgoing_exchange]) - applicator = OrderCycleFormApplicator.new(oc, []) + applicator = OrderCycleFormApplicator.new(oc, user) - applicator.should_receive(:exchange_variant_ids).with(outgoing_exchange).and_return([1, 3]) + applicator.should_receive(:outgoing_exchange_variant_ids).with(outgoing_exchange).and_return([1, 3]) applicator.should_receive(:exchange_exists?).with(coordinator_id, distributor_id, false).and_return(true) applicator.should_receive(:update_exchange).with(coordinator_id, distributor_id, false, {:variant_ids => [1, 3], :enterprise_fee_ids => [1, 2], :pickup_time => 'pickup time', :pickup_instructions => 'pickup instructions'}) applicator.should_receive(:destroy_untouched_exchanges) @@ -95,7 +99,7 @@ module OpenFoodNetwork :incoming_exchanges => [], :outgoing_exchanges => []) - applicator = OrderCycleFormApplicator.new(oc, []) + applicator = OrderCycleFormApplicator.new(oc, user) applicator.should_receive(:destroy_untouched_exchanges) @@ -108,7 +112,7 @@ module OpenFoodNetwork e2 = double(:exchange2, id: 1, foo: 2) oc = double(:order_cycle, :exchanges => [e1]) - applicator = OrderCycleFormApplicator.new(oc, []) + applicator = OrderCycleFormApplicator.new(oc, user) applicator.instance_eval do @touched_exchanges = [e2] end @@ -116,23 +120,118 @@ module OpenFoodNetwork applicator.send(:untouched_exchanges).should == [] end - it "does not destroy exchanges involving enterprises it does not have permission to touch" do - applicator = OrderCycleFormApplicator.new(nil, []) - exchanges = double(:exchanges) - permitted_exchanges = [double(:exchange), double(:exchange)] + context "as a manager of the coordinator" do + let(:applicator) { OrderCycleFormApplicator.new(nil, user) } + before { applicator.stub(:manages_coordinator?) { true } } - applicator.should_receive(:with_permission).with(exchanges) { permitted_exchanges } - applicator.stub(:untouched_exchanges) { exchanges } - permitted_exchanges.each { |ex| ex.should_receive(:destroy) } + it "destroys exchanges" do + exchanges = [double(:exchange), double(:exchange)] + expect(applicator).to receive(:untouched_exchanges) { exchanges } + exchanges.each { |ex| expect(ex).to receive(:destroy) } - applicator.send(:destroy_untouched_exchanges) + applicator.send(:destroy_untouched_exchanges) + end + end + + context "as a non-manager of the coordinator" do + let(:applicator) { OrderCycleFormApplicator.new(nil, user) } + before { applicator.stub(:manages_coordinator?) { false } } + + it "does not destroy any exchanges" do + expect(applicator).to_not receive(:with_permission) + applicator.send(:destroy_untouched_exchanges) + end end end - it "converts exchange variant ids hash to an array of ids" do - applicator = OrderCycleFormApplicator.new(nil, []) + describe "finding alterable exchange variants" do + let(:coordinator_mock) { double(:enterprise) } + let(:oc) { double(:oc, coordinator: coordinator_mock ) } + let!(:applicator) { OrderCycleFormApplicator.new(oc, user) } - applicator.send(:exchange_variant_ids, {:enterprise_id => 123, :variants => {'1' => true, '2' => false, '3' => true}}).should == [1, 3] + describe "converting the existing variants of an exchange to a hash" do + context "when nil is passed in" do + let(:hash) { applicator.send(:persisted_variants_hash, nil) } + + it "returns an empty hash" do + expect(hash).to eq({}) + end + end + + context "when an exchange is passed in" do + let(:v1) { create(:variant) } + let(:exchange) { create(:exchange, variants: [v1]) } + let(:hash) { applicator.send(:persisted_variants_hash, exchange) } + + it "returns a hash with variant ids as keys an all values set to true" do + expect(hash.length).to be 1 + expect(hash[v1.id]).to be true + end + end + end + + context "where a matching exchange does not exist" do + let(:enterprise_mock) { double(:enterprise) } + + before do + applicator.stub(:find_outgoing_exchange) { nil } + expect(applicator).to receive(:editable_variant_ids_for_outgoing_exchange_between). + with(coordinator_mock, enterprise_mock) { [1, 2, 3] } + end + + it "converts exchange variant ids hash to an array of ids" do + applicator.stub(:persisted_variants_hash) { {} } + expect(Enterprise).to receive(:find) { enterprise_mock } + ids = applicator.send(:outgoing_exchange_variant_ids, {:enterprise_id => 123, :variants => {'1' => true, '2' => false, '3' => true}}) + expect(ids).to eq [1, 3] + end + + it "restricts exchange variant ids to those editable by the current user" do + applicator.stub(:persisted_variants_hash) { {4 => true} } + expect(Enterprise).to receive(:find) { enterprise_mock } + ids = applicator.send(:outgoing_exchange_variant_ids, {:enterprise_id => 123, :variants => {'1' => true, '2' => false, '3' => true, '4' => false}}) + expect(ids).to eq [1, 3, 4] + end + + it "overrides existing variants based on submitted data, when user has permission" do + applicator.stub(:persisted_variants_hash) { {2 => true} } + expect(Enterprise).to receive(:find) { enterprise_mock} + ids = applicator.send(:outgoing_exchange_variant_ids, {:enterprise_id => 123, :variants => {'1' => true, '2' => false, '3' => true}}) + expect(ids).to eq [1, 3] + end + end + + context "where a matching exchange exists" do + let(:enterprise_mock) { double(:enterprise) } + let(:exchange_mock) { double(:exchange) } + + before do + applicator.stub(:find_outgoing_exchange) { exchange_mock } + expect(applicator).to receive(:editable_variant_ids_for_outgoing_exchange_between). + with(coordinator_mock, enterprise_mock) { [1, 2, 3] } + end + + it "converts exchange variant ids hash to an array of ids" do + applicator.stub(:persisted_variants_hash) { {} } + expect(exchange_mock).to receive(:receiver) { enterprise_mock } + ids = applicator.send(:outgoing_exchange_variant_ids, {:enterprise_id => 123, :variants => {'1' => true, '2' => false, '3' => true}}) + expect(ids).to eq [1, 3] + end + + it "restricts exchange variant ids to those editable by the current user" do + applicator.stub(:persisted_variants_hash) { {4 => true} } + expect(exchange_mock).to receive(:receiver) { enterprise_mock } + ids = applicator.send(:outgoing_exchange_variant_ids, {:enterprise_id => 123, :variants => {'1' => true, '2' => false, '3' => true, '4' => false}}) + expect(ids).to eq [1, 3, 4] + end + + it "overrides existing variants based on submitted data, when user has permission" do + applicator.stub(:persisted_variants_hash) { {2 => true} } + expect(exchange_mock).to receive(:receiver) { enterprise_mock } + ids = applicator.send(:outgoing_exchange_variant_ids, {:enterprise_id => 123, :variants => {'1' => true, '2' => false, '3' => true}}) + expect(ids).to eq [1, 3] + end + end end describe "filtering exchanges for permission" do @@ -141,7 +240,9 @@ module OpenFoodNetwork e = double(:enterprise) ex = double(:exchange, participant: e) - applicator = OrderCycleFormApplicator.new(nil, [e]) + applicator = OrderCycleFormApplicator.new(nil, user) + applicator.stub(:permitted_enterprises) { [e] } + applicator.send(:permission_for, ex).should be_true end @@ -149,17 +250,10 @@ module OpenFoodNetwork e = double(:enterprise) ex = double(:exchange, participant: e) - applicator = OrderCycleFormApplicator.new(nil, []) - applicator.send(:permission_for, ex).should be_false - end - end + applicator = OrderCycleFormApplicator.new(nil, user) + applicator.stub(:permitted_enterprises) { [] } - describe "filtering many exchanges" do - it "returns exchanges involving enterprises we have permission to touch" do - ex1, ex2 = double(:exchange), double(:exchange) - applicator = OrderCycleFormApplicator.new(nil, []) - applicator.stub(:permission_for).and_return(true, false) - applicator.send(:with_permission, [ex1, ex2]).should == [ex1] + applicator.send(:permission_for, ex).should be_false end end end @@ -173,7 +267,7 @@ module OpenFoodNetwork it "checks whether exchanges exist" do oc = FactoryGirl.create(:simple_order_cycle) exchange = FactoryGirl.create(:exchange, order_cycle: oc) - applicator = OrderCycleFormApplicator.new(oc, []) + applicator = OrderCycleFormApplicator.new(oc, user) applicator.send(:exchange_exists?, exchange.sender_id, exchange.receiver_id, exchange.incoming).should be_true applicator.send(:exchange_exists?, exchange.sender_id, exchange.receiver_id, !exchange.incoming).should be_false @@ -183,60 +277,126 @@ module OpenFoodNetwork applicator.send(:exchange_exists?, 999999, 888888, exchange.incoming).should be_false end - it "adds exchanges" do - sender = FactoryGirl.create(:enterprise) - receiver = FactoryGirl.create(:enterprise) - oc = FactoryGirl.create(:simple_order_cycle) - applicator = OrderCycleFormApplicator.new(oc, [sender, receiver]) - incoming = true - variant1 = FactoryGirl.create(:variant) - variant2 = FactoryGirl.create(:variant) - enterprise_fee1 = FactoryGirl.create(:enterprise_fee) - enterprise_fee2 = FactoryGirl.create(:enterprise_fee) + describe "adding exchanges" do + let!(:sender) { create(:enterprise) } + let!(:receiver) { create(:enterprise) } + let!(:oc) { create(:simple_order_cycle) } + let!(:applicator) { OrderCycleFormApplicator.new(oc, user) } + let!(:incoming) { true } + let!(:variant1) { create(:variant) } + let!(:variant2) { create(:variant) } + let!(:enterprise_fee1) { create(:enterprise_fee) } + let!(:enterprise_fee2) { create(:enterprise_fee) } - applicator.send(:touched_exchanges=, []) - applicator.send(:add_exchange, sender.id, receiver.id, incoming, {:variant_ids => [variant1.id, variant2.id], :enterprise_fee_ids => [enterprise_fee1.id, enterprise_fee2.id]}) + context "as a manager of the coorindator" do + before do + allow(applicator).to receive(:manages_coordinator?) { true } + applicator.send(:touched_exchanges=, []) + applicator.send(:add_exchange, sender.id, receiver.id, incoming, {:variant_ids => [variant1.id, variant2.id], :enterprise_fee_ids => [enterprise_fee1.id, enterprise_fee2.id]}) + end - exchange = Exchange.last - exchange.sender.should == sender - exchange.receiver.should == receiver - exchange.incoming.should == incoming - exchange.variants.sort.should == [variant1, variant2].sort - exchange.enterprise_fees.sort.should == [enterprise_fee1, enterprise_fee2].sort + it "adds new exchanges" do + exchange = Exchange.last + expect(exchange.sender).to eq sender + expect(exchange.receiver).to eq receiver + expect(exchange.incoming).to eq incoming + expect(exchange.variants.sort).to eq [variant1, variant2].sort + expect(exchange.enterprise_fees.sort).to eq [enterprise_fee1, enterprise_fee2].sort - applicator.send(:touched_exchanges).should == [exchange] + applicator.send(:touched_exchanges).should == [exchange] + end + end + + context "as a user which does not manage the coorindator" do + before do + allow(applicator).to receive(:manages_coordinator?) { false } + applicator.send(:add_exchange, sender.id, receiver.id, incoming, {:variant_ids => [variant1.id, variant2.id], :enterprise_fee_ids => [enterprise_fee1.id, enterprise_fee2.id]}) + end + + it "does not add new exchanges" do + expect(Exchange.last).to be_nil + end + end end - it "updates exchanges" do - sender = FactoryGirl.create(:enterprise) - receiver = FactoryGirl.create(:enterprise) - oc = FactoryGirl.create(:simple_order_cycle) - applicator = OrderCycleFormApplicator.new(oc, [sender, receiver]) + describe "updating exchanges" do + let!(:sender) { create(:enterprise) } + let!(:receiver) { create(:enterprise) } + let!(:oc) { create(:simple_order_cycle) } + let!(:applicator) { OrderCycleFormApplicator.new(oc, user) } + let!(:incoming) { true } + let!(:variant1) { create(:variant) } + let!(:variant2) { create(:variant) } + let!(:variant3) { create(:variant) } + let!(:enterprise_fee1) { create(:enterprise_fee) } + let!(:enterprise_fee2) { create(:enterprise_fee) } + let!(:enterprise_fee3) { create(:enterprise_fee) } - incoming = true - variant1 = FactoryGirl.create(:variant) - variant2 = FactoryGirl.create(:variant) - variant3 = FactoryGirl.create(:variant) - enterprise_fee1 = FactoryGirl.create(:enterprise_fee) - enterprise_fee2 = FactoryGirl.create(:enterprise_fee) - enterprise_fee3 = FactoryGirl.create(:enterprise_fee) + let!(:exchange) { create(:exchange, order_cycle: oc, sender: sender, receiver: receiver, incoming: incoming, variant_ids: [variant1.id, variant2.id], enterprise_fee_ids: [enterprise_fee1.id, enterprise_fee2.id]) } - exchange = FactoryGirl.create(:exchange, order_cycle: oc, sender: sender, receiver: receiver, incoming: incoming, variant_ids: [variant1.id, variant2.id], enterprise_fee_ids: [enterprise_fee1.id, enterprise_fee2.id]) + context "as a manager of the coorindator" do + before do + allow(applicator).to receive(:manages_coordinator?) { true } + allow(applicator).to receive(:manager_for) { false } + allow(applicator).to receive(:permission_for) { true } + applicator.send(:touched_exchanges=, []) + applicator.send(:update_exchange, sender.id, receiver.id, incoming, {:variant_ids => [variant1.id, variant3.id], :enterprise_fee_ids => [enterprise_fee2.id, enterprise_fee3.id], :pickup_time => 'New Pickup Time', :pickup_instructions => 'New Pickup Instructions'}) + end - applicator.send(:touched_exchanges=, []) - applicator.send(:update_exchange, sender.id, receiver.id, incoming, {:variant_ids => [variant1.id, variant3.id], :enterprise_fee_ids => [enterprise_fee2.id, enterprise_fee3.id]}) + it "updates the variants, enterprise fees and pickup information of the exchange" do + exchange.reload + expect(exchange.variants.sort).to eq [variant1, variant3].sort + expect(exchange.enterprise_fees.sort).to eq [enterprise_fee2, enterprise_fee3] + expect(exchange.pickup_time).to eq 'New Pickup Time' + expect(exchange.pickup_instructions).to eq 'New Pickup Instructions' + expect(applicator.send(:touched_exchanges)).to eq [exchange] + end + end - exchange.reload - exchange.variants.sort.should == [variant1, variant3].sort - exchange.enterprise_fees.sort.should == [enterprise_fee2, enterprise_fee3] - applicator.send(:touched_exchanges).should == [exchange] + context "as a manager of the participating enterprise" do + before do + allow(applicator).to receive(:manages_coordinator?) { false } + allow(applicator).to receive(:manager_for) { true } + allow(applicator).to receive(:permission_for) { true } + applicator.send(:touched_exchanges=, []) + applicator.send(:update_exchange, sender.id, receiver.id, incoming, {:variant_ids => [variant1.id, variant3.id], :enterprise_fee_ids => [enterprise_fee2.id, enterprise_fee3.id], :pickup_time => 'New Pickup Time', :pickup_instructions => 'New Pickup Instructions'}) + end + + it "updates the variants, enterprise fees and pickup information of the exchange" do + exchange.reload + expect(exchange.variants.sort).to eq [variant1, variant3].sort + expect(exchange.enterprise_fees.sort).to eq [enterprise_fee2, enterprise_fee3] + expect(exchange.pickup_time).to eq 'New Pickup Time' + expect(exchange.pickup_instructions).to eq 'New Pickup Instructions' + expect(applicator.send(:touched_exchanges)).to eq [exchange] + end + end + + context "where the participating enterprise is permitted for the user" do + before do + allow(applicator).to receive(:manages_coordinator?) { false } + allow(applicator).to receive(:manager_for) { false } + allow(applicator).to receive(:permission_for) { true } + applicator.send(:touched_exchanges=, []) + applicator.send(:update_exchange, sender.id, receiver.id, incoming, {:variant_ids => [variant1.id, variant3.id], :enterprise_fee_ids => [enterprise_fee2.id, enterprise_fee3.id], :pickup_time => 'New Pickup Time', :pickup_instructions => 'New Pickup Instructions'}) + end + + it "updates the variants in the exchange, but not the fees or pickup information" do + exchange.reload + expect(exchange.variants.sort).to eq [variant1, variant3].sort + expect(exchange.enterprise_fees.sort).to eq [enterprise_fee1, enterprise_fee2] + expect(exchange.pickup_time).to_not eq 'New Pickup Time' + expect(exchange.pickup_instructions).to_not eq 'New Pickup Instructions' + expect(applicator.send(:touched_exchanges)).to eq [exchange] + end + end end it "does not add exchanges it is not permitted to touch" do sender = FactoryGirl.create(:enterprise) receiver = FactoryGirl.create(:enterprise) oc = FactoryGirl.create(:simple_order_cycle) - applicator = OrderCycleFormApplicator.new(oc, []) + applicator = OrderCycleFormApplicator.new(oc, user) incoming = true expect do @@ -249,7 +409,7 @@ module OpenFoodNetwork sender = FactoryGirl.create(:enterprise) receiver = FactoryGirl.create(:enterprise) oc = FactoryGirl.create(:simple_order_cycle) - applicator = OrderCycleFormApplicator.new(oc, []) + applicator = OrderCycleFormApplicator.new(oc, user) incoming = true exchange = FactoryGirl.create(:exchange, order_cycle: oc, sender: sender, receiver: receiver, incoming: incoming) variant1 = FactoryGirl.create(:variant) diff --git a/spec/lib/open_food_network/order_cycle_permissions_spec.rb b/spec/lib/open_food_network/order_cycle_permissions_spec.rb new file mode 100644 index 0000000000..d135a3500a --- /dev/null +++ b/spec/lib/open_food_network/order_cycle_permissions_spec.rb @@ -0,0 +1,797 @@ +require 'open_food_network/order_cycle_permissions' + +module OpenFoodNetwork + describe OrderCyclePermissions do + let(:coordinator) { create(:distributor_enterprise) } + let(:hub) { create(:distributor_enterprise) } + let(:producer) { create(:supplier_enterprise) } + let(:user) { double(:user) } + let(:oc) { create(:simple_order_cycle, coordinator: coordinator) } + let(:permissions) { OrderCyclePermissions.new(user, oc) } + + describe "finding enterprises that can be viewed in the order cycle interface" do + context "when permissions are initialized without an order_cycle" do + let(:permissions) { OrderCyclePermissions.new(user, nil) } + + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [coordinator]) } + end + + it "returns an empty scope" do + expect(permissions.visible_enterprises).to be_empty + end + end + + context "as a manager of the coordinator" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [coordinator]) } + end + + it "returns the coordinator itself" do + expect(permissions.visible_enterprises).to include coordinator + end + + context "where P-OC has been granted to the coordinator by other enterprises" do + before do + create(:enterprise_relationship, parent: hub, child: coordinator, permissions_list: [:add_to_order_cycle]) + end + + context "where the coordinator sells any" do + it "returns enterprises which have granted P-OC to the coordinator" do + enterprises = permissions.visible_enterprises + expect(enterprises).to include hub + expect(enterprises).to_not include producer + end + end + + context "where the coordinator sells 'own'" do + before { coordinator.stub(:sells) { 'own' } } + it "returns just the coordinator" do + enterprises = permissions.visible_enterprises + expect(enterprises).to_not include hub, producer + end + end + end + + context "where P-OC has not been granted to the coordinator by other enterprises" do + context "where the other enterprise are already in the order cycle" do + let!(:ex_incoming) { create(:exchange, order_cycle: oc, sender: producer, receiver: coordinator, incoming: true) } + let!(:ex_outgoing) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } + + context "where the coordinator sells any" do + it "returns enterprises which have granted P-OC to the coordinator" do + enterprises = permissions.visible_enterprises + expect(enterprises).to include hub, producer + end + end + + context "where the coordinator sells 'own'" do + before { coordinator.stub(:sells) { 'own' } } + it "returns just the coordinator" do + enterprises = permissions.visible_enterprises + expect(enterprises).to_not include hub, producer + end + end + end + + context "where the other enterprises are not in the order cycle" do + it "returns just the coordinator" do + enterprises = permissions.visible_enterprises + expect(enterprises).to_not include hub, producer + end + end + end + end + + context "as a manager of a hub" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [hub]) } + end + + context "that has granted P-OC to the coordinator" do + before do + create(:enterprise_relationship, parent: hub, child: coordinator, permissions_list: [:add_to_order_cycle]) + end + + context "where my hub is in the order cycle" do + let!(:ex_outgoing) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } + + it "returns my hub" do + enterprises = permissions.visible_enterprises + expect(enterprises).to include hub + expect(enterprises).to_not include producer, coordinator + end + + context "and has been granted P-OC by a producer" do + before do + create(:enterprise_relationship, parent: producer, child: hub, permissions_list: [:add_to_order_cycle]) + end + + context "where the producer is in the order cycle" do + let!(:ex_incoming) { create(:exchange, order_cycle: oc, sender: producer, receiver: coordinator, incoming: true) } + + it "returns the producer" do + enterprises = permissions.visible_enterprises + expect(enterprises).to include producer, hub + end + end + + context "where the producer is not in the order cycle" do + # No incoming exchange + + it "does not return the producer" do + enterprises = permissions.visible_enterprises + expect(enterprises).to_not include producer + end + end + end + + context "and has granted P-OC to a producer" do + before do + create(:enterprise_relationship, parent: hub, child: producer, permissions_list: [:add_to_order_cycle]) + end + + context "where the producer is in the order cycle" do + let!(:ex_incoming) { create(:exchange, order_cycle: oc, sender: producer, receiver: coordinator, incoming: true) } + + it "returns the producer" do + enterprises = permissions.visible_enterprises + expect(enterprises).to include producer, hub + end + end + + context "where the producer is not in the order cycle" do + # No incoming exchange + + it "does not return the producer" do + enterprises = permissions.visible_enterprises + expect(enterprises).to_not include producer + end + end + end + end + + context "where my hub is not in the order cycle" do + # No outgoing exchange for my hub + + it "does not return my hub" do + enterprises = permissions.visible_enterprises + expect(enterprises).to_not include hub, producer, coordinator + end + end + end + + context "that has not granted P-OC to the coordinator" do + it "does not return my hub" do + enterprises = permissions.visible_enterprises + expect(enterprises).to_not include hub, producer, coordinator + end + + context "but is already in the order cycle" do + let!(:ex) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } + + it "returns my hub" do + enterprises = permissions.visible_enterprises + expect(enterprises).to include hub + expect(enterprises).to_not include producer, coordinator + end + + context "and distributes variants distributed by an unmanaged and unpermitted producer" do + before { ex.variants << create(:variant, product: create(:product, supplier: producer)) } + + # TODO: update this when we are confident about P-OCs + it "returns that producer as well" do + enterprises = permissions.visible_enterprises + expect(enterprises).to include producer, hub + expect(enterprises).to_not include coordinator + end + end + end + end + end + + context "as a manager of a producer" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [producer]) } + end + + context "which has granted P-OC to the coordinator" do + before do + create(:enterprise_relationship, parent: producer, child: coordinator, permissions_list: [:add_to_order_cycle]) + end + + context "where my producer is in the order cycle" do + let!(:ex_incoming) { create(:exchange, order_cycle: oc, sender: producer, receiver: coordinator, incoming: true) } + + it "returns my producer" do + enterprises = permissions.visible_enterprises + expect(enterprises).to include producer + expect(enterprises).to_not include hub, coordinator + end + + context "and has been granted P-OC by a hub" do + before do + create(:enterprise_relationship, parent: hub, child: producer, permissions_list: [:add_to_order_cycle]) + end + + context "where the hub is also in the order cycle" do + let!(:ex_outgoing) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } + + it "returns the hub as well" do + enterprises = permissions.visible_enterprises + expect(enterprises).to include producer, hub + expect(enterprises).to_not include coordinator + end + end + + context "where the hub is not in the order cycle" do + # No outgoing exchange + + it "does not return the hub" do + enterprises = permissions.visible_enterprises + expect(enterprises).to_not include hub + end + end + end + + context "and has granted P-OC to a hub" do + before do + create(:enterprise_relationship, parent: producer, child: hub, permissions_list: [:add_to_order_cycle]) + end + + context "where the hub is also in the order cycle" do + let!(:ex_outgoing) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } + + it "returns the hub as well" do + enterprises = permissions.visible_enterprises + expect(enterprises).to include producer, hub + expect(enterprises).to_not include coordinator + end + end + + context "where the hub is not in the order cycle" do + # No outgoing exchange + + it "does not return the hub" do + enterprises = permissions.visible_enterprises + expect(enterprises).to_not include hub + end + end + end + end + + context "where my producer is not in the order cycle" do + # No incoming exchange for producer + + it "does not return my producer" do + enterprises = permissions.visible_enterprises + expect(enterprises).to_not include hub, producer, coordinator + end + end + end + + context "which has not granted P-OC to the coordinator" do + it "does not return my producer" do + enterprises = permissions.visible_enterprises + expect(enterprises).to_not include producer + end + + context "but is already in the order cycle" do + let!(:ex_incoming) { create(:exchange, order_cycle: oc, sender: producer, receiver: coordinator, incoming: true) } + + # TODO: update this when we are confident about P-OCs + it "returns my producer" do + enterprises = permissions.visible_enterprises + expect(enterprises).to include producer + expect(enterprises).to_not include hub, coordinator + end + + context "and has variants distributed by an outgoing hub" do + let!(:ex_outgoing) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } + before { ex_outgoing.variants << create(:variant, product: create(:product, supplier: producer)) } + + # TODO: update this when we are confident about P-OCs + it "returns that hub as well" do + enterprises = permissions.visible_enterprises + expect(enterprises).to include producer, hub + expect(enterprises).to_not include coordinator + end + end + end + end + end + end + + describe "finding exchanges of an order cycle that an admin can manage" do + describe "as the manager of the coordinator" do + let!(:ex_in) { create(:exchange, order_cycle: oc, sender: producer, receiver: coordinator, incoming: true) } + let!(:ex_out) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } + + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [coordinator]) } + end + + it "returns all exchanges in the order cycle, regardless of hubE permissions" do + permissions.visible_exchanges.should include ex_in, ex_out + end + end + + + describe "as the manager of a hub" do + let!(:ex_in) { create(:exchange, order_cycle: oc, sender: producer, receiver: coordinator, incoming: true) } + + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [hub]) } + end + + context "where my hub is in the order cycle" do + let!(:ex_out) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } + + it "returns my hub's outgoing exchange" do + permissions.visible_exchanges.should == [ex_out] + end + + context "where my hub has been granted P-OC by an incoming producer" do + before do + create(:enterprise_relationship, parent: producer, child: hub, permissions_list: [:add_to_order_cycle]) + end + + it "returns the producer's incoming exchange" do + permissions.visible_exchanges.should include ex_in + end + end + + context "where my hub has not been granted P-OC by an incoming producer" do + it "returns the producers's incoming exchange, and my own outhoing exchange" do + permissions.visible_exchanges.should_not include ex_in + end + end + end + + context "where my hub isn't in the order cycle" do + it "does not return the producer's incoming exchanges" do + permissions.visible_exchanges.should == [] + end + end + + # TODO: this is testing legacy behaviour for backwards compatability, remove when behaviour no longer required + describe "legacy compatability" do + context "where my hub's outgoing exchange contains variants of a producer I don't manage and has not given my hub P-OC" do + let!(:product) { create(:product, supplier: producer) } + let!(:variant) { create(:variant, product: product) } + let!(:ex_out) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: true) } + before { ex_out.variants << variant } + + it "returns incoming exchanges supplying the variants in my outgoing exchange" do + permissions.visible_exchanges.should include ex_out + end + end + end + end + + describe "as the manager of a producer" do + let!(:ex_out) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } + + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [producer]) } + end + + context "where my producer supplies to the order cycle" do + let!(:ex_in) { create(:exchange, order_cycle: oc, sender: producer, receiver: coordinator, incoming: true) } + + it "returns my producer's incoming exchange" do + permissions.visible_exchanges.should == [ex_in] + end + + context "my producer has granted P-OC to an outgoing hub" do + before do + create(:enterprise_relationship, parent: producer, child: hub, permissions_list: [:add_to_order_cycle]) + end + + it "returns the hub's outgoing exchange" do + permissions.visible_exchanges.should include ex_out + end + end + + context "my producer has not granted P-OC to an outgoing hub" do + it "does not return the hub's outgoing exchange" do + permissions.visible_exchanges.should_not include ex_out + end + end + end + + context "where my producer doesn't supply the order cycle" do + it "does not return the hub's outgoing exchanges" do + permissions.visible_exchanges.should == [] + end + end + + # TODO: this is testing legacy behaviour for backwards compatability, remove when behaviour no longer required + describe "legacy compatability" do + context "where an outgoing exchange contains variants of a producer I manage" do + let!(:product) { create(:product, supplier: producer) } + let!(:variant) { create(:variant, product: product) } + before { ex_out.variants << variant } + + context "where my producer supplies to the order cycle" do + let!(:ex_in) { create(:exchange, order_cycle: oc, sender: producer, receiver: coordinator, incoming: true) } + + it "returns the outgoing exchange" do + permissions.visible_exchanges.should include ex_out + end + end + + context "where my producer doesn't supply to the order cycle" do + it "does not return the outgoing exchange" do + permissions.visible_exchanges.should_not include ex_out + end + end + end + end + end + end + + + describe "finding the variants within a hypothetical exchange between two enterprises which are visible to a user" do + let!(:producer1) { create(:supplier_enterprise) } + let!(:producer2) { create(:supplier_enterprise) } + let!(:v1) { create(:variant, product: create(:simple_product, supplier: producer1)) } + let!(:v2) { create(:variant, product: create(:simple_product, supplier: producer2)) } + + describe "incoming exchanges" do + context "as a manager of the coordinator" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [coordinator]) } + end + + it "returns all variants belonging to the sending producer" do + visible = permissions.visible_variants_for_incoming_exchanges_from(producer1) + expect(visible).to include v1 + expect(visible).to_not include v2 + end + end + + context "as a manager of the producer" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [producer1]) } + end + + it "returns all variants belonging to the sending producer" do + visible = permissions.visible_variants_for_incoming_exchanges_from(producer1) + expect(visible).to include v1 + expect(visible).to_not include v2 + end + end + + context "as a manager of a hub which has been granted P-OC by the producer" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [hub]) } + create(:enterprise_relationship, parent: producer1, child: hub, permissions_list: [:add_to_order_cycle]) + end + + context "where the hub is in the order cycle" do + let!(:ex) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } + + it "returns variants produced by that producer only" do + visible = permissions.visible_variants_for_incoming_exchanges_from(producer1) + expect(visible).to include v1 + expect(visible).to_not include v2 + end + end + + context "where the hub is not in the order cycle" do + # No outgoing exchange + + it "does not return variants produced by that producer" do + visible = permissions.visible_variants_for_incoming_exchanges_from(producer1) + expect(visible).to_not include v1, v2 + end + end + end + end + + describe "outgoing exchanges" do + context "as a manager of the coordinator" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [coordinator]) } + create(:enterprise_relationship, parent: producer1, child: hub, permissions_list: [:add_to_order_cycle]) + end + + it "returns all variants of any producer which has granted the outgoing hub P-OC" do + visible = permissions.visible_variants_for_outgoing_exchanges_to(hub) + expect(visible).to include v1 + expect(visible).to_not include v2 + end + + context "where the coordinator produces products" do + let!(:v3) { create(:variant, product: create(:simple_product, supplier: coordinator)) } + + it "returns any variants produced by the coordinator itself for exchanges with 'self'" do + visible = permissions.visible_variants_for_outgoing_exchanges_to(coordinator) + expect(visible).to include v3 + expect(visible).to_not include v1, v2 + end + + it "does not return coordinator's variants for exchanges with other hubs, when permission has not been granted" do + visible = permissions.visible_variants_for_outgoing_exchanges_to(hub) + expect(visible).to include v1 + expect(visible).to_not include v2, v3 + end + end + + # TODO: for backwards compatability, remove later + context "when an exchange exists between the coordinator and the hub within this order cycle" do + let!(:ex) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } + + # producer2 produces v2 and has not granted P-OC to hub (or coordinator for that matter) + before { ex.variants << v2 } + + it "returns those variants that are in the exchange" do + visible = permissions.visible_variants_for_outgoing_exchanges_to(hub) + expect(visible).to include v1, v2 + end + end + end + + context "as manager of an outgoing hub" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [hub]) } + create(:enterprise_relationship, parent: producer1, child: hub, permissions_list: [:add_to_order_cycle]) + end + + it "returns all variants of any producer which has granted the outgoing hub P-OC" do + visible = permissions.visible_variants_for_outgoing_exchanges_to(hub) + expect(visible).to include v1 + expect(visible).to_not include v2 + end + + # TODO: for backwards compatability, remove later + context "when an exchange exists between the coordinator and the hub within this order cycle" do + let!(:ex) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } + + # producer2 produces v2 and has not granted P-OC to hub + before { ex.variants << v2 } + + it "returns those variants that are in the exchange" do + visible = permissions.visible_variants_for_outgoing_exchanges_to(hub) + expect(visible).to include v1, v2 + end + end + end + + context "as the manager of a producer which has granted P-OC to an outgoing hub" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [producer1]) } + create(:enterprise_relationship, parent: producer1, child: hub, permissions_list: [:add_to_order_cycle]) + end + + context "where my producer is in the order cycle" do + let!(:ex) { create(:exchange, order_cycle: oc, sender: producer1, receiver: coordinator, incoming: true) } + + it "returns all of my produced variants" do + visible = permissions.visible_variants_for_outgoing_exchanges_to(hub) + expect(visible).to include v1 + expect(visible).to_not include v2 + end + end + + context "where my producer isn't in the order cycle" do + # No incoming exchange + + it "does not return my variants" do + visible = permissions.visible_variants_for_outgoing_exchanges_to(hub) + expect(visible).to_not include v1, v2 + end + end + end + + context "as the manager of a producer which has not granted P-OC to an outgoing hub" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [producer2]) } + create(:enterprise_relationship, parent: producer1, child: hub, permissions_list: [:add_to_order_cycle]) + end + + it "returns an empty array" do + expect(permissions.visible_variants_for_outgoing_exchanges_to(hub)).to eq [] + end + + # TODO: for backwards compatability, remove later + context "but which has variants already in the exchange" do + let!(:ex) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } + # This one won't be in the exchange, and so shouldn't be visible + let!(:v3) { create(:variant, product: create(:simple_product, supplier: producer2)) } + + before { ex.variants << v2 } + + it "returns those variants that are in the exchange" do + visible = permissions.visible_variants_for_outgoing_exchanges_to(hub) + expect(visible).to_not include v1, v3 + expect(visible).to include v2 + end + end + end + end + end + + describe "finding the variants within a hypothetical exchange between two enterprises which are editable by a user" do + let!(:producer1) { create(:supplier_enterprise) } + let!(:producer2) { create(:supplier_enterprise) } + let!(:v1) { create(:variant, product: create(:simple_product, supplier: producer1)) } + let!(:v2) { create(:variant, product: create(:simple_product, supplier: producer2)) } + + describe "incoming exchanges" do + context "as a manager of the coordinator" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [coordinator]) } + end + + it "returns all variants belonging to the sending producer" do + visible = permissions.editable_variants_for_incoming_exchanges_from(producer1) + expect(visible).to include v1 + expect(visible).to_not include v2 + end + end + + context "as a manager of the producer" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [producer1]) } + end + + it "returns all variants belonging to the sending producer" do + visible = permissions.editable_variants_for_incoming_exchanges_from(producer1) + expect(visible).to include v1 + expect(visible).to_not include v2 + end + end + + context "as a manager of a hub which has been granted P-OC by the producer" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [hub]) } + create(:enterprise_relationship, parent: producer1, child: hub, permissions_list: [:add_to_order_cycle]) + end + + it "does not return variants produced by that producer" do + visible = permissions.editable_variants_for_incoming_exchanges_from(producer1) + expect(visible).to_not include v1, v2 + end + end + end + + describe "outgoing exchanges" do + context "as a manager of the coordinator" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [coordinator]) } + create(:enterprise_relationship, parent: producer1, child: hub, permissions_list: [:add_to_order_cycle]) + end + + it "returns all variants of any producer which has granted the outgoing hub P-OC" do + visible = permissions.editable_variants_for_outgoing_exchanges_to(hub) + expect(visible).to include v1 + expect(visible).to_not include v2 + end + + context "where the coordinator produces products" do + let!(:v3) { create(:variant, product: create(:simple_product, supplier: coordinator)) } + + it "returns any variants produced by the coordinator itself for exchanges with 'self'" do + visible = permissions.editable_variants_for_outgoing_exchanges_to(coordinator) + expect(visible).to include v3 + expect(visible).to_not include v1, v2 + end + + it "does not return coordinator's variants for exchanges with other hubs, when permission has not been granted" do + visible = permissions.editable_variants_for_outgoing_exchanges_to(hub) + expect(visible).to include v1 + expect(visible).to_not include v2, v3 + end + end + + # TODO: for backwards compatability, remove later + context "when an exchange exists between the coordinator and the hub within this order cycle" do + let!(:ex) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } + + # producer2 produces v2 and has not granted P-OC to hub (or coordinator for that matter) + before { ex.variants << v2 } + + it "returns those variants that are in the exchange" do + visible = permissions.editable_variants_for_outgoing_exchanges_to(hub) + expect(visible).to include v1, v2 + end + end + end + + context "as manager of an outgoing hub" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [hub]) } + create(:enterprise_relationship, parent: producer1, child: hub, permissions_list: [:add_to_order_cycle]) + end + + it "returns all variants of any producer which has granted the outgoing hub P-OC" do + visible = permissions.editable_variants_for_outgoing_exchanges_to(hub) + expect(visible).to include v1 + expect(visible).to_not include v2 + end + + # TODO: for backwards compatability, remove later + context "when an exchange exists between the coordinator and the hub within this order cycle" do + let!(:ex) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } + + # producer2 produces v2 and has not granted P-OC to hub + before { ex.variants << v2 } + + it "returns those variants that are in the exchange" do + visible = permissions.editable_variants_for_outgoing_exchanges_to(hub) + expect(visible).to include v1, v2 + end + end + end + + context "as the manager of a producer which has granted P-OC to an outgoing hub" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [producer1]) } + create(:enterprise_relationship, parent: producer1, child: hub, permissions_list: [:add_to_order_cycle]) + end + + context "where my producer is in the order cycle" do + let!(:ex) { create(:exchange, order_cycle: oc, sender: producer1, receiver: coordinator, incoming: true) } + + context "where the outgoing hub has granted P-OC to my producer" do + before do + create(:enterprise_relationship, parent: hub, child: producer1, permissions_list: [:add_to_order_cycle]) + end + + it "returns all of my produced variants" do + visible = permissions.editable_variants_for_outgoing_exchanges_to(hub) + expect(visible).to include v1 + expect(visible).to_not include v2 + end + end + + context "where the outgoing hub has not granted P-OC to my producer" do + # No permission granted + + it "does not return my variants" do + visible = permissions.editable_variants_for_outgoing_exchanges_to(hub) + expect(visible).to_not include v1, v2 + end + end + end + + context "where my producer isn't in the order cycle" do + # No incoming exchange + + it "does not return my variants" do + visible = permissions.editable_variants_for_outgoing_exchanges_to(hub) + expect(visible).to_not include v1, v2 + end + end + end + + context "as the manager of a producer which has not granted P-OC to an outgoing hub" do + before do + permissions.stub(:managed_enterprises) { Enterprise.where(id: [producer2]) } + create(:enterprise_relationship, parent: producer1, child: hub, permissions_list: [:add_to_order_cycle]) + end + + it "returns an empty array" do + expect(permissions.editable_variants_for_outgoing_exchanges_to(hub)).to eq [] + end + + # TODO: for backwards compatability, remove later + context "but which has variants already in the exchange" do + let!(:ex) { create(:exchange, order_cycle: oc, sender: coordinator, receiver: hub, incoming: false) } + # This one won't be in the exchange, and so shouldn't be visible + let!(:v3) { create(:variant, product: create(:simple_product, supplier: producer2)) } + + before { ex.variants << v2 } + + it "does not return my variants" do + visible = permissions.editable_variants_for_outgoing_exchanges_to(hub) + expect(visible).to_not include v1, v2, v3 + end + end + end + end + end + end +end diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb index f28956d34b..88611b857c 100644 --- a/spec/lib/open_food_network/permissions_spec.rb +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -108,36 +108,6 @@ module OpenFoodNetwork end end - describe "finding exchanges of an order cycle that an admin can manage" do - let(:oc) { create(:simple_order_cycle) } - let!(:ex) { create(:exchange, order_cycle: oc, sender: e1, receiver: e2) } - - before do - permissions.stub(:managed_enterprises) { Enterprise.where('1=0') } - permissions.stub(:related_enterprises_with) { Enterprise.where('1=0') } - end - - it "returns exchanges involving enterprises managed by the user" do - permissions.stub(:managed_enterprises) { Enterprise.where(id: [e1, e2]) } - permissions.order_cycle_exchanges(oc).should == [ex] - end - - it "returns exchanges involving enterprises with E2E permission" do - permissions.stub(:related_enterprises_with) { Enterprise.where(id: [e1, e2]) } - permissions.order_cycle_exchanges(oc).should == [ex] - end - - it "does not return exchanges involving only the sender" do - permissions.stub(:managed_enterprises) { Enterprise.where(id: [e1]) } - permissions.order_cycle_exchanges(oc).should == [] - end - - it "does not return exchanges involving only the receiver" do - permissions.stub(:managed_enterprises) { Enterprise.where(id: [e2]) } - permissions.order_cycle_exchanges(oc).should == [] - end - end - describe "finding managed products" do let!(:p1) { create(:simple_product) } let!(:p2) { create(:simple_product) } diff --git a/spec/lib/open_food_network/sales_tax_report_spec.rb b/spec/lib/open_food_network/sales_tax_report_spec.rb new file mode 100644 index 0000000000..00640d8f08 --- /dev/null +++ b/spec/lib/open_food_network/sales_tax_report_spec.rb @@ -0,0 +1,60 @@ +require 'open_food_network/sales_tax_report' + +module OpenFoodNetwork + describe SalesTaxReport do + let(:report) { SalesTaxReport.new(nil) } + + describe "calculating totals for line items" do + let(:li1) { double(:line_item, quantity: 1, amount: 12) } + let(:li2) { double(:line_item, quantity: 2, amount: 24) } + let(:totals) { report.send(:totals_of, [li1, li2]) } + + before do + report.stub(:tax_included_in).and_return(2, 4) + end + + it "calculates total quantity" do + totals[:items].should == 3 + end + + it "calculates total price" do + totals[:items_total].should == 36 + end + + context "when floating point math would result in fractional cents" do + let(:li1) { double(:line_item, quantity: 1, amount: 0.11) } + let(:li2) { double(:line_item, quantity: 2, amount: 0.12) } + + it "rounds to the nearest cent" do + totals[:items_total].should == 0.23 + end + end + + it "calculates the taxable total price" do + totals[:taxable_total].should == 36 + end + + it "calculates sales tax" do + totals[:sales_tax].should == 6 + end + + context "when there is no tax on a line item" do + before do + report.stub(:tax_included_in) { 0 } + end + + it "does not appear in taxable total" do + totals[:taxable_total].should == 0 + end + + it "still appears on items total" do + totals[:items_total].should == 36 + end + + it "does not register sales tax" do + totals[:sales_tax].should == 0 + end + end + end + end +end diff --git a/spec/mailers/order_mailer_spec.rb b/spec/mailers/order_mailer_spec.rb index 3b8de23e24..a472e95979 100644 --- a/spec/mailers/order_mailer_spec.rb +++ b/spec/mailers/order_mailer_spec.rb @@ -16,7 +16,8 @@ describe Spree::OrderMailer do product = create(:product) product_distribution = create(:product_distribution, :product => product, :distributor => @distributor) @shipping_instructions = "pick up on thursday please!" - @order1 = create(:order, :distributor => @distributor, :bill_address => @bill_address, :special_instructions => @shipping_instructions) + ship_address = create(:address, :address1 => "distributor address", :city => 'The Shire', :zipcode => "1234") + @order1 = create(:order, :distributor => @distributor, :bill_address => @bill_address, ship_address: ship_address, :special_instructions => @shipping_instructions) ActionMailer::Base.deliveries = [] end diff --git a/spec/models/enterprise_group_spec.rb b/spec/models/enterprise_group_spec.rb index 3e38882064..ceca13b13b 100644 --- a/spec/models/enterprise_group_spec.rb +++ b/spec/models/enterprise_group_spec.rb @@ -51,5 +51,14 @@ describe EnterpriseGroup do EnterpriseGroup.on_front_page.should == [eg1] end + + it "finds a user's enterprise groups" do + user = create(:user) + user.spree_roles = [] + eg1 = create(:enterprise_group, owner: user) + eg2 = create(:enterprise_group) + + EnterpriseGroup.managed_by(user).should == [eg1] + end end end diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 494a543260..97a4b988d8 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -29,10 +29,9 @@ describe Enterprise do end it "sends a welcome email" do - mail_message = double "Mail::Message" - expect(EnterpriseMailer).to receive(:welcome).and_return mail_message - mail_message.should_receive :deliver - create(:enterprise, owner: user, email: enterprise.email, confirmed_at: nil) + expect do + create(:enterprise, owner: user, email: enterprise.email, confirmed_at: nil) + end.to enqueue_job WelcomeEnterpriseJob end end end @@ -65,10 +64,9 @@ describe Enterprise do unconfirmed_enterprise.unconfirmed_email = nil unconfirmed_enterprise.save! - mail_message = double "Mail::Message" - expect(EnterpriseMailer).to receive(:welcome).and_return mail_message - mail_message.should_receive :deliver - unconfirmed_enterprise.confirm! + expect do + unconfirmed_enterprise.confirm! + end.to enqueue_job WelcomeEnterpriseJob, enterprise_id: unconfirmed_enterprise.id end end @@ -594,46 +592,72 @@ describe Enterprise do describe "callbacks" do describe "after creation" do let(:owner) { create(:user, enterprise_limit: 10) } - let(:hub1) { create(:distributor_enterprise, owner: owner) } let(:hub2) { create(:distributor_enterprise, owner: owner) } - let(:producer) { create(:supplier_enterprise, owner: owner) } + let(:hub3) { create(:distributor_enterprise, owner: owner) } + let(:producer1) { create(:supplier_enterprise, owner: owner) } + let(:producer2) { create(:supplier_enterprise, owner: owner) } - let(:er1) { EnterpriseRelationship.where(child_id: hub1).last } - let(:er2) { EnterpriseRelationship.where(child_id: hub2).last } - let(:er3) { EnterpriseRelationship.where(child_id: producer).last } - let(:er4) { EnterpriseRelationship.where(parent_id: hub1).last } - let(:er5) { EnterpriseRelationship.where(parent_id: hub2).last } - let(:er6) { EnterpriseRelationship.where(parent_id: producer).last } - - it "establishes bi-directional relationships for new hubs with the owner's hubs and producers" do - hub1 - hub2 - producer - enterprise = nil - - expect do - enterprise = create(:enterprise, owner: owner) - end.to change(EnterpriseRelationship, :count).by(6) - - [er1, er2, er3].each do |er| - er.parent.should == enterprise - er.permissions.map(&:name).sort.should == ['add_to_order_cycle', 'manage_products', 'edit_profile', 'create_variant_overrides'].sort + describe "when a producer is created" do + before do + hub1 + hub2 end - [er4, er5, er6].each do |er| - er.child.should == enterprise - er.permissions.map(&:name).sort.should == ['add_to_order_cycle', 'manage_products', 'edit_profile', 'create_variant_overrides'].sort + it "creates links from the new producer to all hubs owned by the same user, granting add_to_order_cycle and create_variant_overrides permissions" do + producer1 + + should_have_enterprise_relationship from: producer1, to: hub1, with: [:add_to_order_cycle, :create_variant_overrides] + should_have_enterprise_relationship from: producer1, to: hub2, with: [:add_to_order_cycle, :create_variant_overrides] + end + + it "does not create any other links" do + expect do + producer1 + end.to change(EnterpriseRelationship, :count).by(2) end end - it "establishes bi-directional relationships when producers are created" do - hub1 - hub2 - expect do - producer - end.to change(EnterpriseRelationship, :count).by(4) + describe "when a new hub is created" do + it "it creates links to the hub, from all producers owned by the same user, granting add_to_order_cycle and create_variant_overrides permissions" do + producer1 + producer2 + hub1 + + should_have_enterprise_relationship from: producer1, to: hub1, with: [:add_to_order_cycle, :create_variant_overrides] + should_have_enterprise_relationship from: producer2, to: hub1, with: [:add_to_order_cycle, :create_variant_overrides] + end + + + it "creates links from the new hub to all hubs owned by the same user, granting add_to_order_cycle permission" do + hub1 + hub2 + hub3 + + should_have_enterprise_relationship from: hub2, to: hub1, with: [:add_to_order_cycle] + should_have_enterprise_relationship from: hub3, to: hub1, with: [:add_to_order_cycle] + should_have_enterprise_relationship from: hub3, to: hub2, with: [:add_to_order_cycle] + end + + it "does not create any other links" do + producer1 + producer2 + expect { hub1 }.to change(EnterpriseRelationship, :count).by(2) # 2 producer links + expect { hub2 }.to change(EnterpriseRelationship, :count).by(3) # 2 producer links + 1 hub link + expect { hub3 }.to change(EnterpriseRelationship, :count).by(4) # 2 producer links + 2 hub links + end + end + + + def should_have_enterprise_relationship(opts={}) + er = EnterpriseRelationship.where(parent_id: opts[:from], child_id: opts[:to]).last + er.should_not be_nil + if opts[:with] == :all_permissions + er.permissions.map(&:name).sort.should == ['add_to_order_cycle', 'manage_products', 'edit_profile', 'create_variant_overrides'].sort + elsif opts.key? :with + er.permissions.map(&:name).sort.should == opts[:with].map(&:to_s).sort + end end end end diff --git a/spec/models/exchange_spec.rb b/spec/models/exchange_spec.rb index 0dfceadbad..82f360aa16 100644 --- a/spec/models/exchange_spec.rb +++ b/spec/models/exchange_spec.rb @@ -177,6 +177,12 @@ describe Exchange do Exchange.to_enterprises([coordinator]).should == [incoming_exchange] Exchange.to_enterprises([coordinator, distributor]).sort.should == [incoming_exchange, outgoing_exchange].sort end + + it "finds exchanges involving any of a number of enterprises" do + Exchange.involving([supplier]).should == [incoming_exchange] + Exchange.involving([coordinator]).sort.should == [incoming_exchange, outgoing_exchange].sort + Exchange.involving([distributor]).should == [outgoing_exchange] + end end describe "finding exchanges supplying to a distributor" do diff --git a/spec/models/model_set_spec.rb b/spec/models/model_set_spec.rb index 24f452412f..e45018747c 100644 --- a/spec/models/model_set_spec.rb +++ b/spec/models/model_set_spec.rb @@ -3,14 +3,14 @@ require 'spec_helper' describe ModelSet do describe "updating" do it "creates new models" do - attrs = {collection_attributes: {'1' => {name: 'e1', description: 'foo'}, - '2' => {name: 'e2', description: 'bar'}}} + attrs = {collection_attributes: {'1' => {name: 's1'}, + '2' => {name: 's2'}}} - ms = ModelSet.new(EnterpriseGroup, EnterpriseGroup.all, attrs) + ms = ModelSet.new(Suburb, Suburb.all, attrs) - expect { ms.save }.to change(EnterpriseGroup, :count).by(2) + expect { ms.save }.to change(Suburb, :count).by(2) - EnterpriseGroup.where(name: ['e1', 'e2']).count.should == 2 + Suburb.where(name: ['s1', 's2']).count.should == 2 end diff --git a/spec/models/order_cycle_spec.rb b/spec/models/order_cycle_spec.rb index 7ba2b66589..817d2e8e93 100644 --- a/spec/models/order_cycle_spec.rb +++ b/spec/models/order_cycle_spec.rb @@ -90,6 +90,17 @@ describe OrderCycle do end end + describe "#recently_closed" do + it "finds the orders closed in the last 30 days sorted in descending order" do + create(:simple_order_cycle, orders_close_at: 3.days.from_now) + oc1 = create(:simple_order_cycle, orders_close_at: 1.day.ago) + oc2 = create(:simple_order_cycle, orders_close_at: 30.days.ago) + create(:simple_order_cycle, orders_close_at: 31.days.ago) + + OrderCycle.recently_closed.should == [oc1 , oc2] + end + end + it "finds the most recently closed order cycles" do oc1 = create(:simple_order_cycle, orders_close_at: 2.hours.ago) oc2 = create(:simple_order_cycle, orders_close_at: 1.hour.ago) diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index 30fbbd8ab6..0dc9364967 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -24,6 +24,7 @@ module Spree it { subject.can_manage_products?(user).should be_true } it { subject.can_manage_enterprises?(user).should be_true } it { subject.can_manage_orders?(user).should be_true } + it { subject.can_manage_order_cycles?(user).should be_true } end context "as manager of an enterprise who sell 'own'" do @@ -34,6 +35,7 @@ module Spree it { subject.can_manage_products?(user).should be_true } it { subject.can_manage_enterprises?(user).should be_true } it { subject.can_manage_orders?(user).should be_true } + it { subject.can_manage_order_cycles?(user).should be_true } end context "as manager of an enterprise who sells 'none'" do @@ -44,6 +46,7 @@ module Spree it { subject.can_manage_products?(user).should be_false } it { subject.can_manage_enterprises?(user).should be_true } it { subject.can_manage_orders?(user).should be_false } + it { subject.can_manage_order_cycles?(user).should be_false } end context "as manager of a producer enterprise who sells 'any'" do @@ -54,6 +57,7 @@ module Spree it { subject.can_manage_products?(user).should be_true } it { subject.can_manage_enterprises?(user).should be_true } it { subject.can_manage_orders?(user).should be_true } + it { subject.can_manage_order_cycles?(user).should be_true } end context "as manager of a producer enterprise who sell 'own'" do @@ -64,6 +68,7 @@ module Spree it { subject.can_manage_products?(user).should be_true } it { subject.can_manage_enterprises?(user).should be_true } it { subject.can_manage_orders?(user).should be_true } + it { subject.can_manage_order_cycles?(user).should be_true } end context "as manager of a producer enterprise who sells 'none'" do @@ -81,6 +86,7 @@ module Spree it { subject.can_manage_products?(user).should be_true } it { subject.can_manage_enterprises?(user).should be_true } it { subject.can_manage_orders?(user).should be_false } + it { subject.can_manage_order_cycles?(user).should be_false } end context "as a profile" do @@ -93,6 +99,7 @@ module Spree it { subject.can_manage_products?(user).should be_false } it { subject.can_manage_enterprises?(user).should be_true } it { subject.can_manage_orders?(user).should be_false } + it { subject.can_manage_order_cycles?(user).should be_false } end end @@ -100,6 +107,7 @@ module Spree it { subject.can_manage_products?(user).should be_false } it { subject.can_manage_enterprises?(user).should be_false } it { subject.can_manage_orders?(user).should be_false } + it { subject.can_manage_order_cycles?(user).should be_false } it "can create enterprises straight off the bat" do subject.is_new_user?(user).should be_true @@ -212,6 +220,48 @@ module Spree should_not have_ability([:sales_total, :group_buys, :payments, :orders_and_distributors, :users_and_enterprises], for: :report) end + describe "order_cycles abilities" do + context "where the enterprise is not in an order_cycle" do + let!(:order_cycle) { create(:simple_order_cycle) } + + it "should not be able to access read/update order_cycle actions" do + should_not have_ability([:admin, :index, :read, :edit, :update], for: order_cycle) + end + + it "should not be able to access bulk_update, clone order cycle actions" do + should_not have_ability([:bulk_update, :clone], for: order_cycle) + end + + it "cannot request permitted enterprises for an order cycle" do + should_not have_ability([:for_order_cycle], for: Enterprise) + end + + it "cannot request permitted enterprise fees for an order cycle" do + should_not have_ability([:for_order_cycle], for: EnterpriseFee) + end + end + + context "where the enterprise is in an order_cycle" do + let!(:order_cycle) { create(:simple_order_cycle) } + let!(:exchange){ create(:exchange, incoming: true, order_cycle: order_cycle, receiver: order_cycle.coordinator, sender: s1) } + + it "should be able to access read/update order cycle actions" do + should have_ability([:admin, :index, :read, :edit, :update], for: order_cycle) + end + + it "should not be able to access bulk/update, clone order cycle actions" do + should_not have_ability([:bulk_update, :clone], for: order_cycle) + end + + it "can request permitted enterprises for an order cycle" do + should have_ability([:for_order_cycle], for: Enterprise) + end + + it "can request permitted enterprise fees for an order cycle" do + should have_ability([:for_order_cycle], for: EnterpriseFee) + end + end + end end context "when is a distributor enterprise user" do @@ -350,13 +400,33 @@ module Spree end it "should be able to read some reports" do - should have_ability([:admin, :index, :customers, :group_buys, :bulk_coop, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory], for: :report) + should have_ability([:admin, :index, :customers, :sales_tax, :group_buys, :bulk_coop, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory], for: :report) end it "should not be able to read other reports" do should_not have_ability([:sales_total, :users_and_enterprises], for: :report) end + context "for a given order_cycle" do + let!(:order_cycle) { create(:simple_order_cycle) } + let!(:exchange){ create(:exchange, incoming: false, order_cycle: order_cycle, receiver: d1, sender: order_cycle.coordinator) } + + it "should be able to access read and update order cycle actions" do + should have_ability([:admin, :index, :read, :edit, :update], for: order_cycle) + end + + it "should not be able to access bulk_update, clone order cycle actions" do + should_not have_ability([:bulk_update, :clone], for: order_cycle) + end + end + + it "can request permitted enterprises for an order cycle" do + should have_ability([:for_order_cycle], for: Enterprise) + end + + it "can request permitted enterprise fees for an order cycle" do + should have_ability([:for_order_cycle], for: EnterpriseFee) + end end context 'Order Cycle co-ordinator, distributor enterprise manager' do @@ -371,11 +441,11 @@ module Spree let(:oc2) { create(:simple_order_cycle) } it "should be able to read/write OrderCycles they are the co-ordinator of" do - should have_ability([:admin, :index, :read, :edit, :update, :clone], for: oc1) + should have_ability([:admin, :index, :read, :edit, :update, :bulk_update, :clone, :destroy], for: oc1) end it "should not be able to read/write OrderCycles they are not the co-ordinator of" do - should_not have_ability([:admin, :index, :read, :create, :edit, :update, :clone], for: oc2) + should_not have_ability([:admin, :index, :read, :create, :edit, :update, :bulk_update, :clone, :destroy], for: oc2) end it "should be able to create OrderCycles" do @@ -383,7 +453,7 @@ module Spree end it "should be able to read/write EnterpriseFees" do - should have_ability([:admin, :index, :read, :create, :edit, :bulk_update, :destroy], for: EnterpriseFee) + should have_ability([:admin, :index, :read, :create, :edit, :bulk_update, :destroy, :for_order_cycle], for: EnterpriseFee) end it "should be able to add enterprises to order cycles" do diff --git a/spec/models/spree/adjustment_spec.rb b/spec/models/spree/adjustment_spec.rb index 60dfd631ce..e69d04da6f 100644 --- a/spec/models/spree/adjustment_spec.rb +++ b/spec/models/spree/adjustment_spec.rb @@ -4,5 +4,153 @@ module Spree adjustment = create(:adjustment, metadata: create(:adjustment_metadata)) adjustment.metadata.should be end + + describe "recording included tax" do + describe "TaxRate adjustments" do + let!(:zone) { create(:zone_with_member) } + let!(:order) { create(:order, bill_address: create(:address)) } + let!(:line_item) { create(:line_item, order: order) } + let(:tax_rate) { create(:tax_rate, included_in_price: true, calculator: Calculator::FlatRate.new(preferred_amount: 0.1)) } + let(:adjustment) { line_item.adjustments(:reload).first } + + before do + order.reload + tax_rate.adjust(order) + end + + it "has 100% tax included" do + adjustment.amount.should be > 0 + adjustment.included_tax.should == adjustment.amount + end + + it "does not crash when order data has been updated previously" do + order.price_adjustments.first.destroy + tax_rate.adjust(order) + end + end + + describe "Shipment adjustments" do + let!(:order) { create(:order, shipping_method: shipping_method) } + let!(:line_item) { create(:line_item, order: order) } + let(:shipping_method) { create(:shipping_method, calculator: Calculator::FlatRate.new(preferred_amount: 50.0)) } + let(:adjustment) { order.adjustments(:reload).shipping.first } + + it "has a shipping charge of $50" do + order.create_shipment! + adjustment.amount.should == 50 + end + + describe "when tax on shipping is disabled" do + it "records 0% tax on shipment adjustments" do + Config.shipment_inc_vat = false + Config.shipping_tax_rate = 0 + order.create_shipment! + + adjustment.included_tax.should == 0 + end + + it "records 0% tax on shipments when a rate is set but shipment_inc_vat is false" do + Config.shipment_inc_vat = false + Config.shipping_tax_rate = 0.25 + order.create_shipment! + + adjustment.included_tax.should == 0 + end + end + + describe "when tax on shipping is enabled" do + before do + Config.shipment_inc_vat = true + Config.shipping_tax_rate = 0.25 + order.create_shipment! + end + + it "takes the shipment adjustment tax included from the system setting" do + # Finding the tax included in an amount that's already inclusive of tax: + # total - ( total / (1 + rate) ) + # 50 - ( 50 / (1 + 0.25) ) + # = 10 + adjustment.included_tax.should == 10.00 + end + + it "records 0% tax on shipments when shipping_tax_rate is not set" do + Config.shipment_inc_vat = true + Config.shipping_tax_rate = nil + order.create_shipment! + + adjustment.included_tax.should == 0 + end + end + end + + describe "EnterpriseFee adjustments" do + let!(:zone) { create(:zone_with_member) } + let(:tax_rate) { create(:tax_rate, included_in_price: true, calculator: Calculator::DefaultTax.new, zone: zone, amount: 0.1) } + let(:tax_category) { create(:tax_category, tax_rates: [tax_rate]) } + + let(:coordinator) { create(:distributor_enterprise) } + let(:variant) { create(:variant) } + let(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator, coordinator_fees: [enterprise_fee], distributors: [coordinator], variants: [variant]) } + let!(:order) { create(:order, order_cycle: order_cycle, distributor: coordinator) } + let!(:line_item) { create(:line_item, order: order, variant: variant) } + let(:adjustment) { order.adjustments(:reload).enterprise_fee.first } + + before do + order.reload.update_distribution_charge! + end + + context "when enterprise fees are taxed per-order" do + let(:enterprise_fee) { create(:enterprise_fee, enterprise: coordinator, tax_category: tax_category, calculator: Calculator::FlatRate.new(preferred_amount: 50.0)) } + + it "records the tax on the enterprise fee adjustments" do + # The fee is $50, tax is 10%, and the fee is inclusive of tax + # Therefore, the included tax should be 0.1/1.1 * 50 = $4.55 + + adjustment.included_tax.should == 4.55 + end + + describe "when the tax rate does not include the tax in the price" do + before do + tax_rate.update_attribute :included_in_price, false + order.update_distribution_charge! + end + + it "treats it as inclusive anyway" do + adjustment.included_tax.should == 4.55 + end + end + + describe "when enterprise fees have no tax" do + before do + enterprise_fee.tax_category = nil + enterprise_fee.save! + order.update_distribution_charge! + end + + it "records no tax as charged" do + adjustment.included_tax.should == 0 + end + end + end + + + context "when enterprise fees are taxed per-item" do + let(:enterprise_fee) { create(:enterprise_fee, enterprise: coordinator, tax_category: tax_category, calculator: Calculator::PerItem.new(preferred_amount: 50.0)) } + + it "records the tax on the enterprise fee adjustments" do + adjustment.included_tax.should == 4.55 + end + end + end + + describe "setting the included tax by tax rate" do + let(:adjustment) { Adjustment.new label: 'foo', amount: 50 } + + it "sets it, rounding to two decimal places" do + adjustment.set_included_tax! 0.25 + adjustment.included_tax.should == 10.00 + end + end + end end end diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index 114f774a1d..3c70c2243c 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -186,6 +186,57 @@ describe Spree::Order do end end + describe "getting the shipping tax" do + let(:order) { create(:order, shipping_method: shipping_method) } + let(:shipping_method) { create(:shipping_method, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 50.0)) } + + context "with a taxed shipment" do + before do + Spree::Config.shipment_inc_vat = true + Spree::Config.shipping_tax_rate = 0.25 + order.create_shipment! + end + + it "returns the shipping tax" do + order.shipping_tax.should == 10 + end + end + + it "returns zero when the order has not been shipped" do + order.shipping_tax.should == 0 + end + end + + describe "getting the enterprise fee tax" do + let!(:order) { create(:order) } + let(:enterprise_fee1) { create(:enterprise_fee) } + let(:enterprise_fee2) { create(:enterprise_fee) } + let!(:adjustment1) { create(:adjustment, adjustable: order, originator: enterprise_fee1, label: "EF 1", amount: 123, included_tax: 10.00) } + let!(:adjustment2) { create(:adjustment, adjustable: order, originator: enterprise_fee2, label: "EF 2", amount: 123, included_tax: 2.00) } + + it "returns a sum of the tax included in all enterprise fees" do + order.reload.enterprise_fee_tax.should == 12 + end + end + + describe "getting the total tax" do + let(:order) { create(:order, shipping_method: shipping_method) } + let(:shipping_method) { create(:shipping_method, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 50.0)) } + let(:enterprise_fee) { create(:enterprise_fee) } + let!(:adjustment) { create(:adjustment, adjustable: order, originator: enterprise_fee, label: "EF", amount: 123, included_tax: 2) } + + before do + Spree::Config.shipment_inc_vat = true + Spree::Config.shipping_tax_rate = 0.25 + order.create_shipment! + order.reload + end + + it "returns a sum of all tax on the order" do + order.total_tax.should == 12 + end + end + describe "setting the distributor" do it "sets the distributor when no order cycle is set" do d = create(:distributor_enterprise) @@ -337,7 +388,7 @@ describe Spree::Order do end end - describe "with payment method name" do + describe "with payment method names" do let!(:o1) { create(:order) } let!(:o2) { create(:order) } let!(:pm1) { create(:payment_method, name: 'foo') } @@ -345,8 +396,12 @@ describe Spree::Order do let!(:p1) { create(:payment, order: o1, payment_method: pm1) } let!(:p2) { create(:payment, order: o2, payment_method: pm2) } - it "returns the order with payment method name" do - Spree::Order.with_payment_method_name('foo').should == [o1] + it "returns the order with payment method name when one specified" do + Spree::Order.with_payment_method_name('foo').should == [o1] + end + + it "returns the orders with payment method name when many specified" do + Spree::Order.with_payment_method_name(['foo', 'bar']).should include o1, o2 end it "doesn't return rows with a different payment method name" do @@ -355,8 +410,8 @@ describe Spree::Order do end it "doesn't return duplicate rows" do - p2 = FactoryGirl.create(:payment, :order => o1, :payment_method => pm1) - Spree::Order.with_payment_method_name('foo').length.should == 1 + p2 = FactoryGirl.create(:payment, order: o1, payment_method: pm1) + Spree::Order.with_payment_method_name('foo').length.should == 1 end end end @@ -397,14 +452,10 @@ describe Spree::Order do end describe "sending confirmation emails" do - it "sends confirmation emails to both the user and the shop owner" do - customer_confirm_fake = double(:confirm_email_for_customer) - shop_confirm_fake = double(:confirm_email_for_shop) - expect(Spree::OrderMailer).to receive(:confirm_email_for_customer).and_return customer_confirm_fake - expect(Spree::OrderMailer).to receive(:confirm_email_for_shop).and_return shop_confirm_fake - expect(customer_confirm_fake).to receive :deliver - expect(shop_confirm_fake).to receive :deliver - create(:order).deliver_order_confirmation_email + it "sends confirmation emails" do + expect do + create(:order).deliver_order_confirmation_email + end.to enqueue_job ConfirmOrderJob end end end diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index c739b4378b..848febb9c5 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -24,12 +24,34 @@ describe Spree.user_class do }.to raise_error ActiveRecord::RecordInvalid, "Validation failed: #{u2.email} is not permitted to own any more enterprises (limit is 1)." end end + + describe "group ownership" do + let(:u1) { create(:user) } + let(:u2) { create(:user) } + let!(:g1) { create(:enterprise_group, owner: u1) } + let!(:g2) { create(:enterprise_group, owner: u1) } + let!(:g3) { create(:enterprise_group, owner: u2) } + + it "provides access to owned groups" do + expect(u1.owned_groups(:reload)).to match_array([g1, g2]) + expect(u2.owned_groups(:reload)).to match_array([g3]) + end + end + + it "loads a user's customer representation at a particular enterprise" do + u = create(:user) + e = create(:enterprise) + c = create(:customer, user: u, enterprise: e) + + u.customer_of(e).should == c + end end context "#create" do it "should send a signup email" do - Spree::UserMailer.should_receive(:signup_confirmation).and_return(double(:deliver => true)) - create(:user) + expect do + create(:user) + end.to enqueue_job ConfirmSignupJob end end diff --git a/spec/models/spree/variant_spec.rb b/spec/models/spree/variant_spec.rb index 85deecc6ea..5a82f04ba7 100644 --- a/spec/models/spree/variant_spec.rb +++ b/spec/models/spree/variant_spec.rb @@ -104,6 +104,44 @@ module Spree end end + describe "generating the full name" do + let(:v) { Variant.new } + + before do + v.stub(:display_name) { 'display_name' } + v.stub(:unit_to_display) { 'unit_to_display' } + end + + it "returns unit_to_display when display_name is blank" do + v.stub(:display_name) { '' } + v.full_name.should == 'unit_to_display' + end + + it "returns display_name when it contains unit_to_display" do + v.stub(:display_name) { 'DiSpLaY_name' } + v.stub(:unit_to_display) { 'name' } + v.full_name.should == 'DiSpLaY_name' + end + + it "returns unit_to_display when it contains display_name" do + v.stub(:display_name) { '_to_' } + v.stub(:unit_to_display) { 'unit_TO_display' } + v.full_name.should == 'unit_TO_display' + end + + it "returns a combination otherwise" do + v.stub(:display_name) { 'display_name' } + v.stub(:unit_to_display) { 'unit_to_display' } + v.full_name.should == 'display_name (unit_to_display)' + end + + it "is resilient to regex chars" do + v = Variant.new display_name: ")))" + v.stub(:unit_to_display) { ")))" } + v.full_name.should == ")))" + end + end + describe "calculating the price with enterprise fees" do it "returns the price plus the fees" do distributor = double(:distributor) diff --git a/spec/serializers/admin/exchange_serializer_spec.rb b/spec/serializers/admin/exchange_serializer_spec.rb new file mode 100644 index 0000000000..b45f6bcb9b --- /dev/null +++ b/spec/serializers/admin/exchange_serializer_spec.rb @@ -0,0 +1,26 @@ +require 'open_food_network/order_cycle_permissions' + +describe Api::Admin::ExchangeSerializer do + let(:v1) { create(:variant) } + let(:v2) { create(:variant) } + let(:exchange) { create(:exchange, incoming: false, variants: [v1, v2]) } + let(:permissions_mock) { double(:permissions) } + let(:serializer) { Api::Admin::ExchangeSerializer.new exchange } + + + before do + allow(OpenFoodNetwork::OrderCyclePermissions).to receive(:new) { permissions_mock } + allow(permissions_mock).to receive(:visible_variants_for_outgoing_exchanges_to) do + # This is the permitted list of variants + Spree::Variant.where(id: [v1] ) + end + end + + it "filters variants within the exchange based on permissions" do + visible_variants = serializer.variants + expect(permissions_mock).to have_received(:visible_variants_for_outgoing_exchanges_to).with(exchange.receiver) + expect(exchange.variants).to include v1, v2 + expect(visible_variants.keys).to include v1.id + expect(visible_variants.keys).to_not include v2.id + end +end diff --git a/spec/serializers/admin/for_order_cycle/enterprise_serializer_spec.rb b/spec/serializers/admin/for_order_cycle/enterprise_serializer_spec.rb new file mode 100644 index 0000000000..db476164eb --- /dev/null +++ b/spec/serializers/admin/for_order_cycle/enterprise_serializer_spec.rb @@ -0,0 +1,13 @@ +describe Api::Admin::ForOrderCycle::EnterpriseSerializer do + let(:enterprise) { create(:distributor_enterprise) } + let!(:product) { create(:simple_product, supplier: enterprise) } + let!(:deleted_product) { create(:simple_product, supplier: enterprise, deleted_at: 24.hours.ago ) } + let(:serialized_enterprise) { Api::Admin::ForOrderCycle::EnterpriseSerializer.new(enterprise, spree_current_user: enterprise.owner ).to_json } + + describe "supplied products" do + it "does not render deleted products" do + expect(serialized_enterprise).to have_json_size(1).at_path 'supplied_products' + expect(serialized_enterprise).to be_json_eql(product.master.id).at_path 'supplied_products/0/master_id' + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3a7d2b3ca1..d5410a5026 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -93,6 +93,7 @@ RSpec.configure do |config| config.include OpenFoodNetwork::EnterpriseGroupsHelper config.include OpenFoodNetwork::DistributionHelper config.include ActionView::Helpers::DateHelper + config.include OpenFoodNetwork::DelayedJobHelper # FactoryGirl require 'factory_girl_rails' diff --git a/spec/support/delayed_job_helper.rb b/spec/support/delayed_job_helper.rb new file mode 100644 index 0000000000..5f9cfb6140 --- /dev/null +++ b/spec/support/delayed_job_helper.rb @@ -0,0 +1,68 @@ +module OpenFoodNetwork + module DelayedJobHelper + def run_job(job) + clear_jobs + Delayed::Job.enqueue job + flush_jobs + end + + + # Process all pending Delayed jobs, keeping in mind jobs could spawn new + # delayed job (so things might be added to the queue while processing) + def flush_jobs(options = {}) + options[:ignore_exceptions] ||= false + + Delayed::Worker.new.work_off(100) + + unless options[:ignore_exceptions] + Delayed::Job.all.each do |job| + if job.last_error.present? + throw "There was an error in a delayed job: #{job.last_error}" + end + end + end + end + + + def clear_jobs + Delayed::Job.delete_all + end + + + # expect { foo }.to enqueue_job MyJob, field1: 'foo', field2: 'bar' + RSpec::Matchers.define :enqueue_job do |klass, options = {}| + match do |event_proc| + last_job_id_before = Delayed::Job.last.andand.id || 0 + + event_proc.call + + @jobs_created = Delayed::Job.where('id > ?', last_job_id_before) + + @jobs_created.any? do |job| + job = job.payload_object + + match = true + match &= (job.class == klass) + + options.each_pair do |k, v| + begin + match &= (job[k] == v) + rescue NameError + match = false + end + end + + match + end + end + + failure_message_for_should do |event_proc| + "expected job to be enqueued matching #{options.inspect} (#{@jobs_created.andand.count || '???'} others enqueued)" + end + + failure_message_for_should_not do |event_proc| + "expected job to not be enqueued matching #{options.inspect}" + end + end + end +end diff --git a/spec/support/matchers/select2_matchers.rb b/spec/support/matchers/select2_matchers.rb index 8b7bc2f834..67cfcd81d0 100644 --- a/spec/support/matchers/select2_matchers.rb +++ b/spec/support/matchers/select2_matchers.rb @@ -14,6 +14,10 @@ RSpec::Matchers.define :have_select2 do |id, options={}| results << node.has_selector?(from) + if results.all? + results << selected_option_is(from, options[:selected]) if options.key? :selected + end + if results.all? results << all_options_present(from, options[:with_options]) if options.key? :with_options results << exact_options_present(from, options[:options]) if options.key? :options @@ -47,6 +51,12 @@ RSpec::Matchers.define :have_select2 do |id, options={}| end end + def selected_option_is(from, text) + within find(from) do + find("a.select2-choice").text == text + end + end + def with_select2_open(from) find(from).click r = yield diff --git a/spec/support/request/authentication_workflow.rb b/spec/support/request/authentication_workflow.rb index 39f78223d7..e71c0dd21a 100644 --- a/spec/support/request/authentication_workflow.rb +++ b/spec/support/request/authentication_workflow.rb @@ -1,5 +1,6 @@ module AuthenticationWorkflow include Warden::Test::Helpers + def quick_login_as(user) login_as user end @@ -37,6 +38,7 @@ module AuthenticationWorkflow visit spree.admin_path end + # TODO: Should probably just rename this to create_user def create_enterprise_user( attrs = {} ) new_user = create(:user, attrs) new_user.spree_roles = [] # for some reason unbeknown to me, this new user gets admin permissions by default. @@ -54,11 +56,8 @@ module AuthenticationWorkflow end def login_to_consumer_section - # The first user is given the admin role by Spree, so create a dummy user if this is the first - create(:user) if Spree::User.admin.empty? - user_role = Spree::Role.find_or_create_by_name!('user') - user = Spree::User.create!({ + user = create_enterprise_user({ :email => 'someone@ofn.org', :password => 'passw0rd', :password_confirmation => 'passw0rd', @@ -69,9 +68,9 @@ module AuthenticationWorkflow user.spree_roles << user_role visit spree.login_path - fill_in 'spree_user_email', :with => 'someone@ofn.org' - fill_in 'spree_user_password', :with => 'passw0rd' - click_button 'Login' + fill_in 'email', :with => 'someone@ofn.org' + fill_in 'password', :with => 'passw0rd' + click_button 'Log in' end end diff --git a/spec/views/admin/enterprises/for_order_cycle.rabl_spec.rb b/spec/views/admin/enterprises/for_order_cycle.rabl_spec.rb deleted file mode 100644 index 1585d08e43..0000000000 --- a/spec/views/admin/enterprises/for_order_cycle.rabl_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'spec_helper' - -describe "admin/enterprises/for_order_cycle.rabl" do - let(:enterprise) { create(:distributor_enterprise) } - let!(:product) { create(:simple_product, supplier: enterprise) } - let!(:deleted_product) { create(:simple_product, supplier: enterprise, deleted_at: 1.day.ago) } - let(:render) { Rabl.render([enterprise], 'admin/enterprises/for_order_cycle', view_path: 'app/views', scope: RablHelper::FakeContext.instance) } - - describe "supplied products" do - it "does not render deleted products" do - render.should have_json_size(1).at_path '0/supplied_products' - render.should be_json_eql(product.master.id).at_path '0/supplied_products/0/master_id' - end - end -end diff --git a/vendor/assets/javascripts/jquery.adaptivemenu.js b/vendor/assets/javascripts/jquery.adaptivemenu.js new file mode 100644 index 0000000000..b9a611a8fb --- /dev/null +++ b/vendor/assets/javascripts/jquery.adaptivemenu.js @@ -0,0 +1,64 @@ +/* + * Original from spree/core/vendor/assets/javascripts/jquery.adaptivemenu.js + */ + +/* + * Used for the spree admin tab bar (Orders, Products, Reports etc.). + * Using parent's width instead of window width. + */ +jQuery.fn.AdaptiveMenu = function(options){ + + var options = jQuery.extend({ + text: "More...", + accuracy:0, // originally 70, but not needed anymore + 'class':null, + 'classLinckMore':null + },options); + + var menu = this; + var li = $(menu).find("li"); + + var width = 0; + var widthLi = []; + $.each( li , function(i, l){ + width += $(l).width(); + widthLi.push( width ); + }); + + var buildingMenu = function(){ + // Using parent width instead of given window width + var windowWidth = $(menu.parent()).width() - options.accuracy; + for(var i = 0; i windowWidth ) + $( li[i] ).hide(); + else + $( li[i] ).show(); + } + $(menu).find('#more').remove(); + var hideLi = $(li).filter(':not(:visible)'); + var lastLi = $(li).filter(':visible').last(); + if ( hideLi.length > 0 ){ + var more = $("
  • ") + .css({"display":"inline-block","white-space":"nowrap"}) + .addClass(options.classLinckMore) + .attr({"id":"more"}) + .html(options.text) + .click(function(){$(this).find('li').toggle()}); + + var ul = $("
      ") + .css({"position":"absolute"}) + .addClass(options.klass) + .html(hideLi.clone()).prepend(lastLi.clone().hide()); + + more.append(ul); + + lastLi.hide().before(more); + } + } + + // calling buildingMenu without parameter jQuery(window).width() + jQuery(window).resize(buildingMenu); + + jQuery(window).ready(buildingMenu); + +};