diff --git a/Gemfile b/Gemfile index 89f1fc61eb..1f289ea8a4 100644 --- a/Gemfile +++ b/Gemfile @@ -9,9 +9,9 @@ gem 'i18n', '~> 0.6.11' gem 'nokogiri', '>= 1.6.7.1' gem 'pg' -gem 'spree', :github => 'openfoodfoundation/spree', :branch => '1-3-stable' -gem 'spree_i18n', :github => 'spree/spree_i18n', :branch => '1-3-stable' -gem 'spree_auth_devise', :github => 'spree/spree_auth_devise', :branch => '1-3-stable' +gem 'spree', github: 'openfoodfoundation/spree', branch: '1-3-stable' +gem 'spree_i18n', github: 'spree/spree_i18n', branch: '1-3-stable' +gem 'spree_auth_devise', github: 'spree/spree_auth_devise', branch: '1-3-stable' # Waiting on merge of PR #117 # https://github.com/spree-contrib/better_spree_paypal_express/pull/117 diff --git a/Gemfile.lock b/Gemfile.lock index 694fa89f56..5d958b23f6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -23,7 +23,7 @@ GIT GIT remote: git://github.com/openfoodfoundation/spree.git - revision: afcc23e489eb604a3e2651598a7c8364e2acc7b3 + revision: 6e3edfe40a5de8eba0095b2c5f3db9ea54c3afda branch: 1-3-stable specs: spree (1.3.6.beta) @@ -54,7 +54,7 @@ GIT rabl (= 0.7.2) rails (~> 3.2.16) ransack (= 0.7.2) - select2-rails (= 3.2.1) + select2-rails (= 3.5.9.3) state_machine (= 1.1.2) stringex (~> 1.3.2) truncate_html (~> 0.5.5) @@ -124,8 +124,8 @@ GEM active_link_to (1.0.0) active_model_serializers (0.8.3) activemodel (>= 3.0) - activemerchant (1.48.0) - activesupport (>= 3.2.14, < 5.0.0) + activemerchant (1.57.0) + activesupport (>= 3.2.14, < 5.1) builder (>= 2.1.2, < 4.0.0) i18n (>= 0.6.9) nokogiri (~> 1.4) @@ -574,7 +574,7 @@ GEM railties (~> 3.2.0) sass (>= 3.1.10) tilt (~> 1.3) - select2-rails (3.2.1) + select2-rails (3.5.9.3) thor (~> 0.14) shoulda-matchers (1.1.0) activesupport (>= 3.0.0) diff --git a/app/assets/images/select2.png b/app/assets/images/select2.png new file mode 100755 index 0000000000..9790e029f0 Binary files /dev/null and b/app/assets/images/select2.png differ diff --git a/app/assets/images/select2x2.png b/app/assets/images/select2x2.png new file mode 100755 index 0000000000..7e737c98cf Binary files /dev/null and b/app/assets/images/select2x2.png differ diff --git a/app/assets/javascripts/admin/admin.js.coffee b/app/assets/javascripts/admin/admin_ofn.js.coffee similarity index 100% rename from app/assets/javascripts/admin/admin.js.coffee rename to app/assets/javascripts/admin/admin_ofn.js.coffee diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 7d3e94194d..344c5e8a5d 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -21,14 +21,16 @@ //= require ../shared/ng-tags-input.min.js //= require angular-rails-templates //= require_tree ../templates/admin -//= require ./admin +//= require ./admin_ofn //= require ./accounts_and_billing_settings/accounts_and_billing_settings //= require ./business_model_configuration/business_model_configuration //= require ./customers/customers //= require ./dropdown/dropdown //= require ./enterprises/enterprises +//= require ./enterprise_fees/enterprise_fees //= require ./enterprise_groups/enterprise_groups //= require ./index_utils/index_utils +//= require ./inventory_items/inventory_items //= require ./line_items/line_items //= require ./orders/orders //= require ./order_cycles/order_cycles diff --git a/app/assets/javascripts/admin/controllers/enterprises_dashboard_controller.js.coffee b/app/assets/javascripts/admin/controllers/enterprises_dashboard_controller.js.coffee index ad72ff3529..60315d30c1 100644 --- a/app/assets/javascripts/admin/controllers/enterprises_dashboard_controller.js.coffee +++ b/app/assets/javascripts/admin/controllers/enterprises_dashboard_controller.js.coffee @@ -1,5 +1,2 @@ -angular.module("ofn.admin").controller "enterprisesDashboardCtrl", [ - "$scope" - ($scope) -> - $scope.activeTab = "hubs" -] \ No newline at end of file +angular.module("ofn.admin").controller "enterprisesDashboardCtrl", ($scope) -> + $scope.activeTab = "hubs" diff --git a/app/assets/javascripts/admin/dropdown/controllers/dropdown_controller.js.coffee b/app/assets/javascripts/admin/dropdown/controllers/dropdown_controller.js.coffee deleted file mode 100644 index 02e47ff9f7..0000000000 --- a/app/assets/javascripts/admin/dropdown/controllers/dropdown_controller.js.coffee +++ /dev/null @@ -1,2 +0,0 @@ -angular.module("admin.dropdown").controller "DropDownCtrl", ($scope) -> - $scope.expanded = false diff --git a/app/assets/javascripts/admin/dropdown/directives/close_on_click.js.coffee b/app/assets/javascripts/admin/dropdown/directives/close_on_click.js.coffee index 9b506cb8fb..1ab9e5c8a4 100644 --- a/app/assets/javascripts/admin/dropdown/directives/close_on_click.js.coffee +++ b/app/assets/javascripts/admin/dropdown/directives/close_on_click.js.coffee @@ -1,4 +1,4 @@ - angular.module("admin.dropdown").directive "ofnCloseOnClick", ($document) -> + angular.module("admin.dropdown").directive "closeOnClick", () -> link: (scope, element, attrs) -> element.click (event) -> event.stopPropagation() diff --git a/app/assets/javascripts/admin/dropdown/directives/dropdown.js.coffee b/app/assets/javascripts/admin/dropdown/directives/dropdown.js.coffee index b4ca2869d7..f26894afbc 100644 --- a/app/assets/javascripts/admin/dropdown/directives/dropdown.js.coffee +++ b/app/assets/javascripts/admin/dropdown/directives/dropdown.js.coffee @@ -1,6 +1,9 @@ angular.module("admin.dropdown").directive "ofnDropDown", ($document) -> restrict: 'C' + scope: true link: (scope, element, attrs) -> + scope.expanded = false + outsideClickListener = (event) -> unless $(event.target).is("div.ofn-drop-down##{attrs.id} div.menu") || $(event.target).parents("div.ofn-drop-down##{attrs.id} div.menu").length > 0 diff --git a/app/assets/javascripts/admin/enterprise_fees.js b/app/assets/javascripts/admin/enterprise_fees.js deleted file mode 100644 index b815cd4266..0000000000 --- a/app/assets/javascripts/admin/enterprise_fees.js +++ /dev/null @@ -1,69 +0,0 @@ -angular.module('enterprise_fees', []) - .controller('AdminEnterpriseFeesCtrl', ['$scope', '$http', '$window', function($scope, $http, $window) { - $scope.enterpriseFeesUrl = function() { - var url = '/admin/enterprise_fees.json?include_calculators=1'; - - var match = $window.location.search.match(/enterprise_id=(\d+)/); - if(match) { - url += "&"+match[0]; - } - - return url; - }; - - $http.get($scope.enterpriseFeesUrl()).success(function(data) { - $scope.enterprise_fees = data; - - // TODO: Angular 1.1.0 will have a means to reset a form to its pristine state, which - // would avoid the need to save off original calculator types for comparison. - for(i in $scope.enterprise_fees) { - $scope.enterprise_fees[i].orig_calculator_type = $scope.enterprise_fees[i].calculator_type; - } - }); - }]) - - .directive('ngBindHtmlUnsafeCompiled', ['$compile', function($compile) { - return function(scope, element, attrs) { - scope.$watch(attrs.ngBindHtmlUnsafeCompiled, function(value) { - element.html($compile(value)(scope)); - }); - } - }]) - - .directive('spreeDeleteResource', function() { - return function(scope, element, attrs) { - if(scope.enterprise_fee.id) { - var url = "/admin/enterprise_fees/" + scope.enterprise_fee.id - var html = ''; - //var html = 'Delete Delete'; - element.append(html); - } - } - }) - - .directive('spreeEnsureCalculatorPreferencesMatchType', function() { - // Hide calculator preference fields when calculator type changed - // Fixes 'Enterprise fee is not found' error when changing calculator type - // See spree/core/app/assets/javascripts/admin/calculator.js - - // Note: For some reason, DOM --> model bindings aren't working here, so - // we use element.val() instead of querying the model itself. - - return function(scope, element, attrs) { - scope.$watch(function(scope) { - //return scope.enterprise_fee.calculator_type; - return element.val(); - }, function(value) { - var settings = element.parent().parent().find("div.calculator-settings"); - - // scope.enterprise_fee.calculator_type == scope.enterprise_fee.orig_calculator_type - if(element.val() == scope.enterprise_fee.orig_calculator_type) { - settings.show(); - settings.find("input").prop("disabled", false); - } else { - settings.hide(); - settings.find("input").prop("disabled", true); - } - }); - } - }); diff --git a/app/assets/javascripts/admin/enterprise_fees/controllers/enterprise_fees_controller.js.coffee b/app/assets/javascripts/admin/enterprise_fees/controllers/enterprise_fees_controller.js.coffee new file mode 100644 index 0000000000..e3cf5ee4cd --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_fees/controllers/enterprise_fees_controller.js.coffee @@ -0,0 +1,14 @@ +angular.module('admin.enterpriseFees').controller 'enterpriseFeesCtrl', ($scope, $http, $window, enterprises, tax_categories, calculators) -> + $scope.enterprises = enterprises + $scope.tax_categories = [{id: -1, name: "Inherit From Product"}].concat tax_categories + $scope.calculators = calculators + + $scope.enterpriseFeesUrl = -> + url = '/admin/enterprise_fees.json?include_calculators=1' + match = $window.location.search.match(/enterprise_id=(\d+)/) + if match + url += '&' + match[0] + url + + $http.get($scope.enterpriseFeesUrl()).success (data) -> + $scope.enterprise_fees = data diff --git a/app/assets/javascripts/admin/enterprise_fees/directives/bind_html_unsafe_compiled.js.coffee b/app/assets/javascripts/admin/enterprise_fees/directives/bind_html_unsafe_compiled.js.coffee new file mode 100644 index 0000000000..96c2292257 --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_fees/directives/bind_html_unsafe_compiled.js.coffee @@ -0,0 +1,6 @@ +angular.module("admin.enterpriseFees").directive 'ngBindHtmlUnsafeCompiled', ($compile) -> + (scope, element, attrs) -> + scope.$watch attrs.ngBindHtmlUnsafeCompiled, (value) -> + element.html $compile(value)(scope) + return + return diff --git a/app/assets/javascripts/admin/enterprise_fees/directives/delete_resource.js.coffee b/app/assets/javascripts/admin/enterprise_fees/directives/delete_resource.js.coffee new file mode 100644 index 0000000000..0ae1b3f6fd --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_fees/directives/delete_resource.js.coffee @@ -0,0 +1,8 @@ +angular.module('admin.enterpriseFees').directive 'spreeDeleteResource', -> + (scope, element, attrs) -> + if scope.enterprise_fee.id + url = '/admin/enterprise_fees/' + scope.enterprise_fee.id + html = '' + #var html = 'Delete Delete'; + element.append html + return diff --git a/app/assets/javascripts/admin/enterprise_fees/directives/ensure_calculator_preferences_match_type.js.coffee b/app/assets/javascripts/admin/enterprise_fees/directives/ensure_calculator_preferences_match_type.js.coffee new file mode 100644 index 0000000000..4376ce59ef --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_fees/directives/ensure_calculator_preferences_match_type.js.coffee @@ -0,0 +1,17 @@ +angular.module("admin.enterpriseFees").directive 'spreeEnsureCalculatorPreferencesMatchType', -> + # Hide calculator preference fields when calculator type changed + # Fixes 'Enterprise fee is not found' error when changing calculator type + # See spree/core/app/assets/javascripts/admin/calculator.js + (scope, element, attrs) -> + orig_calculator_type = scope.enterprise_fee.calculator_type + + scope.$watch "enterprise_fee.calculator_type", (value) -> + settings = element.parent().parent().find('div.calculator-settings') + if value == orig_calculator_type + settings.show() + settings.find('input').prop 'disabled', false + else + settings.hide() + settings.find('input').prop 'disabled', true + return + return diff --git a/app/assets/javascripts/admin/enterprise_fees/directives/watch_tax_category.js.coffee b/app/assets/javascripts/admin/enterprise_fees/directives/watch_tax_category.js.coffee new file mode 100644 index 0000000000..370f34d9f0 --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_fees/directives/watch_tax_category.js.coffee @@ -0,0 +1,15 @@ +angular.module("admin.enterpriseFees").directive 'watchTaxCategory', -> + # In order to have a nice user experience on this page, we're modelling tax_category + # inheritance using tax_category_id = -1. + # This directive acts as a parser for tax_category_id, storing the value the form as "" when + # tax_category is to be inherited and setting inherits_tax_category as appropriate. + (scope, element, attrs) -> + scope.$watch 'enterprise_fee.tax_category_id', (value) -> + if value == -1 + scope.enterprise_fee.inherits_tax_category = true + element.val("") + else + scope.enterprise_fee.inherits_tax_category = false + element.val(value) + + scope.enterprise_fee.tax_category_id = -1 if scope.enterprise_fee.inherits_tax_category diff --git a/app/assets/javascripts/admin/enterprise_fees/enterprise_fees.js b/app/assets/javascripts/admin/enterprise_fees/enterprise_fees.js new file mode 100644 index 0000000000..9c4e2ed4a5 --- /dev/null +++ b/app/assets/javascripts/admin/enterprise_fees/enterprise_fees.js @@ -0,0 +1 @@ +angular.module("admin.enterpriseFees", ['admin.indexUtils']) 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 45a5d068a4..d464585f67 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 @@ -17,6 +17,7 @@ angular.module("admin.enterprises") { 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()" } + { name: "Inventory Settings", icon_class: "icon-list-ol", show: "showInventorySettings()" } { name: "Shop Preferences", icon_class: "icon-shopping-cart", show: "showShopPreferences()" } ] @@ -41,5 +42,8 @@ angular.module("admin.enterprises") $scope.showEnterpriseFees = -> enterprisePermissions.can_manage_enterprise_fees && ($scope.Enterprise.sells != "none" || $scope.Enterprise.is_primary_producer) + $scope.showInventorySettings = -> + $scope.Enterprise.sells != "none" + $scope.showShopPreferences = -> $scope.Enterprise.sells != "none" diff --git a/app/assets/javascripts/admin/index_utils/directives/ofn-select.js.coffee b/app/assets/javascripts/admin/index_utils/directives/ofn-select.js.coffee new file mode 100644 index 0000000000..9a653d24f8 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/directives/ofn-select.js.coffee @@ -0,0 +1,13 @@ +# Mainly useful for adding a blank option that works with AngularJS +# Angular doesn't seem to understand the blank option generated by rails +# using the include_blank flag on select helper. +angular.module("admin.indexUtils").directive "ofnSelect", -> + restrict: 'E' + scope: + data: "=" + replace: true + template: (element, attrs) -> + valueAttr = attrs.valueAttr || 'id' + textAttr = attrs.textAttr || 'name' + blank = if attrs.includeBlank? then "" else "" + return "" diff --git a/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee b/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee index ba7a4b54df..ec454e9216 100644 --- a/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/ofn-select2.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.indexUtils").directive "ofnSelect2", ($timeout, blankOption) -> +angular.module("admin.indexUtils").directive "ofnSelect2", ($sanitize, $timeout) -> require: 'ngModel' restrict: 'C' scope: @@ -10,6 +10,8 @@ angular.module("admin.indexUtils").directive "ofnSelect2", ($timeout, blankOptio $timeout -> scope.text ||= 'name' scope.data.unshift(scope.blank) if scope.blank? && typeof scope.blank is "object" + + item.name = $sanitize(item.name) for item in scope.data element.select2 minimumResultsForSearch: scope.minSearch || 0 data: { results: scope.data, text: scope.text } diff --git a/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee b/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee index 2910e9a7a1..614b8d9346 100644 --- a/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee +++ b/app/assets/javascripts/admin/index_utils/directives/toggle_column.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.indexUtils").directive "ofnToggleColumn", (Columns) -> +angular.module("admin.indexUtils").directive "toggleColumn", (Columns) -> link: (scope, element, attrs) -> element.addClass "selected" if scope.column.visible diff --git a/app/assets/javascripts/admin/index_utils/directives/toggle_view.js.coffee b/app/assets/javascripts/admin/index_utils/directives/toggle_view.js.coffee new file mode 100644 index 0000000000..5741c06fc7 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/directives/toggle_view.js.coffee @@ -0,0 +1,11 @@ +angular.module("admin.indexUtils").directive "toggleView", (Views) -> + link: (scope, element, attrs) -> + Views.register + element.addClass "selected" if scope.view.visible + + element.click "click", -> + scope.$apply -> + Views.selectView(scope.viewKey) + + scope.$watch "view.visible", (newValue, oldValue) -> + element.toggleClass "selected", scope.view.visible diff --git a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee index adcd68e3c5..5e5b5cadf2 100644 --- a/app/assets/javascripts/admin/index_utils/index_utils.js.coffee +++ b/app/assets/javascripts/admin/index_utils/index_utils.js.coffee @@ -1 +1 @@ -angular.module("admin.indexUtils", ['ngResource', 'templates']).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content"); $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"; \ No newline at end of file +angular.module("admin.indexUtils", ['ngResource', 'ngSanitize', 'templates']).config ($httpProvider) -> $httpProvider.defaults.headers.common["X-CSRF-Token"] = $("meta[name=csrf-token]").attr("content"); $httpProvider.defaults.headers.common["Accept"] = "application/json, text/javascript, */*"; \ No newline at end of file diff --git a/app/assets/javascripts/admin/index_utils/services/data_fetcher.js.coffee b/app/assets/javascripts/admin/index_utils/services/data_fetcher.js.coffee index bf5580a3b2..edfb2c0a06 100644 --- a/app/assets/javascripts/admin/index_utils/services/data_fetcher.js.coffee +++ b/app/assets/javascripts/admin/index_utils/services/data_fetcher.js.coffee @@ -1,12 +1,9 @@ -angular.module("admin.indexUtils").factory "dataFetcher", [ - "$http", "$q" - ($http, $q) -> - return (dataLocation) -> - deferred = $q.defer() - $http.get(dataLocation).success((data) -> - deferred.resolve data - ).error -> - deferred.reject() +angular.module("admin.indexUtils").factory "dataFetcher", ($http, $q, RequestMonitor) -> + return (dataLocation) -> + deferred = $q.defer() + RequestMonitor.load $http.get(dataLocation).success((data) -> + deferred.resolve data + ).error -> + deferred.reject() - deferred.promise -] + deferred.promise diff --git a/app/assets/javascripts/admin/index_utils/services/views.js.coffee b/app/assets/javascripts/admin/index_utils/services/views.js.coffee new file mode 100644 index 0000000000..cf26a73ad8 --- /dev/null +++ b/app/assets/javascripts/admin/index_utils/services/views.js.coffee @@ -0,0 +1,16 @@ +angular.module("admin.indexUtils").factory 'Views', ($rootScope) -> + new class Views + views: {} + currentView: null + + setViews: (views) => + @views = {} + for key, view of views + @views[key] = view + @selectView(key) if view.visible + @views + + selectView: (selectedKey) => + @currentView = @views[selectedKey] + for key, view of @views + view.visible = (key == selectedKey) diff --git a/app/assets/javascripts/admin/inventory_items/inventory_items.js.coffee b/app/assets/javascripts/admin/inventory_items/inventory_items.js.coffee new file mode 100644 index 0000000000..5ee31bad4b --- /dev/null +++ b/app/assets/javascripts/admin/inventory_items/inventory_items.js.coffee @@ -0,0 +1 @@ +angular.module("admin.inventoryItems", ['ngResource']) diff --git a/app/assets/javascripts/admin/inventory_items/services/inventory_item_resource.js.coffee b/app/assets/javascripts/admin/inventory_items/services/inventory_item_resource.js.coffee new file mode 100644 index 0000000000..fb2699f854 --- /dev/null +++ b/app/assets/javascripts/admin/inventory_items/services/inventory_item_resource.js.coffee @@ -0,0 +1,5 @@ +angular.module("admin.inventoryItems").factory 'InventoryItemResource', ($resource) -> + $resource('/admin/inventory_items/:id/:action.json', {}, { + 'update': + method: 'PUT' + }) diff --git a/app/assets/javascripts/admin/inventory_items/services/inventory_items.js.coffee b/app/assets/javascripts/admin/inventory_items/services/inventory_items.js.coffee new file mode 100644 index 0000000000..ac7971ad24 --- /dev/null +++ b/app/assets/javascripts/admin/inventory_items/services/inventory_items.js.coffee @@ -0,0 +1,25 @@ +angular.module("admin.inventoryItems").factory "InventoryItems", (inventoryItems, InventoryItemResource) -> + new class InventoryItems + inventoryItems: {} + errors: {} + + constructor: -> + for ii in inventoryItems + @inventoryItems[ii.enterprise_id] ||= {} + @inventoryItems[ii.enterprise_id][ii.variant_id] = new InventoryItemResource(ii) + + setVisibility: (hub_id, variant_id, visible) -> + if @inventoryItems[hub_id] && @inventoryItems[hub_id][variant_id] + inventory_item = angular.extend(angular.copy(@inventoryItems[hub_id][variant_id]), {visible: visible}) + InventoryItemResource.update {id: inventory_item.id}, inventory_item, (data) => + @inventoryItems[hub_id][variant_id] = data + , (response) => + @errors[hub_id] ||= {} + @errors[hub_id][variant_id] = response.data.errors + else + InventoryItemResource.save {enterprise_id: hub_id, variant_id: variant_id, visible: visible}, (data) => + @inventoryItems[hub_id] ||= {} + @inventoryItems[hub_id][variant_id] = data + , (response) => + @errors[hub_id] ||= {} + @errors[hub_id][variant_id] = response.data.errors 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 deleted file mode 100644 index d2e69ad64f..0000000000 --- a/app/assets/javascripts/admin/order_cycles/filters/visible_product_variants.js.coffee +++ /dev/null @@ -1,4 +0,0 @@ -angular.module("admin.orderCycles").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 index 40586854c1..5ce498b998 100644 --- a/app/assets/javascripts/admin/order_cycles/filters/visible_products.js.coffee +++ b/app/assets/javascripts/admin/order_cycles/filters/visible_products.js.coffee @@ -1,3 +1,3 @@ angular.module("admin.orderCycles").filter "visibleProducts", ($filter) -> return (products, exchange, rules) -> - return (product for product in products when $filter('visibleProductVariants')(product, exchange, rules).length > 0) + return (product for product in products when $filter('visibleVariants')(product.variants, exchange, rules).length > 0) diff --git a/app/assets/javascripts/admin/order_cycles/filters/visible_variants.js.coffee b/app/assets/javascripts/admin/order_cycles/filters/visible_variants.js.coffee new file mode 100644 index 0000000000..db261ebcea --- /dev/null +++ b/app/assets/javascripts/admin/order_cycles/filters/visible_variants.js.coffee @@ -0,0 +1,3 @@ +angular.module("admin.orderCycles").filter "visibleVariants", -> + return (variants, exchange, rules) -> + return (variant for variant in variants when variant.id in rules[exchange.enterprise_id]) diff --git a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee index 8c7d798138..0fcd41969c 100644 --- a/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee +++ b/app/assets/javascripts/admin/services/enterprise_relationships.js.coffee @@ -29,4 +29,4 @@ angular.module("ofn.admin").factory 'EnterpriseRelationships', ($http, enterpris when "add_to_order_cycle" then "add to order cycle" when "manage_products" then "manage products" when "edit_profile" then "edit profile" - when "create_variant_overrides" then "override variant details" + when "create_variant_overrides" then "add products to inventory" diff --git a/app/assets/javascripts/admin/taxons/directives/taxon_autocomplete.js.coffee b/app/assets/javascripts/admin/taxons/directives/taxon_autocomplete.js.coffee index b978a050ad..b1eac64569 100644 --- a/app/assets/javascripts/admin/taxons/directives/taxon_autocomplete.js.coffee +++ b/app/assets/javascripts/admin/taxons/directives/taxon_autocomplete.js.coffee @@ -1,4 +1,4 @@ -angular.module("admin.taxons").directive "ofnTaxonAutocomplete", (Taxons) -> +angular.module("admin.taxons").directive "ofnTaxonAutocomplete", (Taxons, $sanitize) -> # Adapted from Spree's existing taxon autocompletion scope: true link: (scope,element,attrs) -> @@ -18,7 +18,7 @@ angular.module("admin.taxons").directive "ofnTaxonAutocomplete", (Taxons) -> query: (query) -> query.callback { results: Taxons.findByTerm(query.term) } formatResult: (taxon) -> - taxon.name + $sanitize(taxon.name) formatSelection: (taxon) -> taxon.name diff --git a/app/assets/javascripts/admin/taxons/taxons.js.coffee b/app/assets/javascripts/admin/taxons/taxons.js.coffee index 863e6e8125..07de167ccf 100644 --- a/app/assets/javascripts/admin/taxons/taxons.js.coffee +++ b/app/assets/javascripts/admin/taxons/taxons.js.coffee @@ -1 +1 @@ -angular.module("admin.taxons", []) \ No newline at end of file +angular.module("admin.taxons", ['ngSanitize']) \ No newline at end of file diff --git a/app/assets/javascripts/admin/users/directives/user_select.js.coffee b/app/assets/javascripts/admin/users/directives/user_select.js.coffee index 94df1894d9..787ef2124b 100644 --- a/app/assets/javascripts/admin/users/directives/user_select.js.coffee +++ b/app/assets/javascripts/admin/users/directives/user_select.js.coffee @@ -1,19 +1,20 @@ -angular.module("admin.users").directive "userSelect", -> +angular.module("admin.users").directive "userSelect", ($sanitize) -> scope: user: '&userSelect' model: '=ngModel' - link: (scope,element,attrs) -> + link: (scope, element, attrs) -> setTimeout -> element.select2 multiple: false initSelection: (element, callback) -> - callback {id: scope.user().id, email: scope.user().email} + callback {id: scope.user()?.id, email: scope.user()?.email} ajax: url: '/admin/search/known_users' datatype: 'json' - data:(term, page) -> + data: (term, page) -> { q: term } results: (data, page) -> + item.email = $sanitize(item.email) for item in data { results: data } formatResult: (user) -> user.email diff --git a/app/assets/javascripts/admin/users/users.js.coffee b/app/assets/javascripts/admin/users/users.js.coffee index 6bfd47a894..a86a90a56c 100644 --- a/app/assets/javascripts/admin/users/users.js.coffee +++ b/app/assets/javascripts/admin/users/users.js.coffee @@ -1 +1 @@ -angular.module("admin.users", []) \ No newline at end of file +angular.module("admin.users", ['admin.utils']) \ No newline at end of file diff --git a/app/assets/javascripts/admin/utils/directives/alert_row.js.coffee b/app/assets/javascripts/admin/utils/directives/alert_row.js.coffee new file mode 100644 index 0000000000..c7bd2840a1 --- /dev/null +++ b/app/assets/javascripts/admin/utils/directives/alert_row.js.coffee @@ -0,0 +1,18 @@ +angular.module("admin.utils").directive "alertRow", -> + restrict: "E" + replace: true + scope: + message: '@' + buttonText: '@?' + buttonAction: '&?' + dismissed: '=?' + close: "&?" + transclude: true + templateUrl: "admin/alert_row.html" + link: (scope, element, attrs) -> + scope.dismissed = false + + scope.dismiss = -> + scope.dismissed = true + scope.close() if scope.close? + return false diff --git a/app/assets/javascripts/admin/utils/directives/with_tip.js.coffee b/app/assets/javascripts/admin/utils/directives/with_tip.js.coffee new file mode 100644 index 0000000000..51bb5b05bf --- /dev/null +++ b/app/assets/javascripts/admin/utils/directives/with_tip.js.coffee @@ -0,0 +1,8 @@ +angular.module("admin.utils").directive "ofnWithTip", ($sanitize)-> + link: (scope, element, attrs) -> + element.attr('data-powertip', $sanitize(attrs.ofnWithTip)) + element.powerTip + smartPlacement: true + fadeInTime: 50 + fadeOutTime: 50 + intentPollInterval: 300 diff --git a/app/assets/javascripts/admin/utils/utils.js.coffee b/app/assets/javascripts/admin/utils/utils.js.coffee index 4d58ae930a..094d3a5849 100644 --- a/app/assets/javascripts/admin/utils/utils.js.coffee +++ b/app/assets/javascripts/admin/utils/utils.js.coffee @@ -1 +1 @@ -angular.module("admin.utils", []) \ No newline at end of file +angular.module("admin.utils", ["ngSanitize"]) \ No newline at end of file diff --git a/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee b/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee index b65df80d66..26d1e05250 100644 --- a/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/controllers/variant_overrides_controller.js.coffee @@ -1,12 +1,23 @@ -angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", ($scope, $http, $timeout, Indexer, Columns, SpreeApiAuth, PagedFetcher, StatusMessage, hubs, producers, hubPermissions, VariantOverrides, DirtyVariantOverrides) -> +angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", ($scope, $http, $timeout, Indexer, Columns, Views, SpreeApiAuth, PagedFetcher, StatusMessage, RequestMonitor, hubs, producers, hubPermissions, InventoryItems, VariantOverrides, DirtyVariantOverrides) -> $scope.hubs = Indexer.index hubs - $scope.hub = null + $scope.hub_id = if hubs.length == 1 then hubs[0].id else null $scope.products = [] $scope.producers = producers $scope.producersByID = Indexer.index producers $scope.hubPermissions = hubPermissions + $scope.productLimit = 10 $scope.variantOverrides = VariantOverrides.variantOverrides + $scope.inventoryItems = InventoryItems.inventoryItems + $scope.setVisibility = InventoryItems.setVisibility $scope.StatusMessage = StatusMessage + $scope.RequestMonitor = RequestMonitor + $scope.selectView = Views.selectView + $scope.currentView = -> Views.currentView + + $scope.views = Views.setViews + inventory: { name: "Inventory Products", visible: true } + hidden: { name: "Hidden Products", visible: false } + new: { name: "New Products", visible: false } $scope.columns = Columns.setColumns producer: { name: "Producer", visible: true } @@ -17,6 +28,9 @@ angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", on_demand: { name: "On Demand", visible: false } reset: { name: "Reset Stock Level", visible: false } inheritance: { name: "Inheritance", visible: false } + visibility: { name: "Hide", visible: false } + + $scope.bulkActions = [ name: "Reset Stock Levels To Defaults", callback: 'resetStock' ] $scope.resetSelectFilters = -> $scope.producerFilter = 0 @@ -24,6 +38,9 @@ angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", $scope.resetSelectFilters() + $scope.filtersApplied = -> + $scope.producerFilter != 0 || $scope.query != '' + $scope.initialise = -> SpreeApiAuth.authorise() .then -> @@ -42,10 +59,6 @@ angular.module("admin.variantOverrides").controller "AdminVariantOverridesCtrl", $scope.products = $scope.products.concat products VariantOverrides.ensureDataFor hubs, products - - $scope.selectHub = -> - $scope.hub = $scope.hubs[$scope.hub_id] - $scope.displayDirty = -> if DirtyVariantOverrides.count() > 0 num = if DirtyVariantOverrides.count() == 1 then "one override" else "#{DirtyVariantOverrides.count()} overrides" diff --git a/app/assets/javascripts/admin/variant_overrides/directives/track_inheritance.js.coffee b/app/assets/javascripts/admin/variant_overrides/directives/track_inheritance.js.coffee index 20ac08c035..e0a67eb7d0 100644 --- a/app/assets/javascripts/admin/variant_overrides/directives/track_inheritance.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/directives/track_inheritance.js.coffee @@ -2,11 +2,11 @@ angular.module("admin.variantOverrides").directive "trackInheritance", (VariantO require: "ngModel" link: (scope, element, attrs, ngModel) -> # This is a bit hacky, but it allows us to load the inherit property on the VO, but then not submit it - scope.inherit = angular.equals scope.variantOverrides[scope.hub.id][scope.variant.id], VariantOverrides.newFor scope.hub.id, scope.variant.id + scope.inherit = angular.equals scope.variantOverrides[scope.hub_id][scope.variant.id], VariantOverrides.newFor scope.hub_id, scope.variant.id ngModel.$parsers.push (viewValue) -> if ngModel.$dirty && viewValue - variantOverride = VariantOverrides.inherit(scope.hub.id, scope.variant.id) + variantOverride = VariantOverrides.inherit(scope.hub_id, scope.variant.id) DirtyVariantOverrides.add variantOverride scope.displayDirty() viewValue diff --git a/app/assets/javascripts/admin/variant_overrides/directives/track_variant_override.js.coffee b/app/assets/javascripts/admin/variant_overrides/directives/track_variant_override.js.coffee index 919533967c..184b7af232 100644 --- a/app/assets/javascripts/admin/variant_overrides/directives/track_variant_override.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/directives/track_variant_override.js.coffee @@ -3,7 +3,7 @@ angular.module("admin.variantOverrides").directive "ofnTrackVariantOverride", (D link: (scope, element, attrs, ngModel) -> ngModel.$parsers.push (viewValue) -> if ngModel.$dirty - variantOverride = scope.variantOverrides[scope.hub.id][scope.variant.id] + variantOverride = scope.variantOverrides[scope.hub_id][scope.variant.id] scope.inherit = false DirtyVariantOverrides.add variantOverride scope.displayDirty() diff --git a/app/assets/javascripts/admin/variant_overrides/filters/inventory_products_filter.js.coffee b/app/assets/javascripts/admin/variant_overrides/filters/inventory_products_filter.js.coffee new file mode 100644 index 0000000000..95b8786902 --- /dev/null +++ b/app/assets/javascripts/admin/variant_overrides/filters/inventory_products_filter.js.coffee @@ -0,0 +1,17 @@ +angular.module("admin.variantOverrides").filter "inventoryProducts", ($filter, InventoryItems) -> + return (products, hub_id, views) -> + return [] if !hub_id + return $filter('filter')(products, (product) -> + for variant in product.variants + if InventoryItems.inventoryItems.hasOwnProperty(hub_id) && InventoryItems.inventoryItems[hub_id].hasOwnProperty(variant.id) + if InventoryItems.inventoryItems[hub_id][variant.id].visible + # Important to only return if true, as other variants for this product might be visible + return true if views.inventory.visible + else + # Important to only return if true, as other variants for this product might be visible + return true if views.hidden.visible + else + # Important to only return if true, as other variants for this product might be visible + return true if views.new.visible + false + , true) diff --git a/app/assets/javascripts/admin/variant_overrides/filters/inventory_variants_filter.js.coffee b/app/assets/javascripts/admin/variant_overrides/filters/inventory_variants_filter.js.coffee new file mode 100644 index 0000000000..81ddc8215f --- /dev/null +++ b/app/assets/javascripts/admin/variant_overrides/filters/inventory_variants_filter.js.coffee @@ -0,0 +1,12 @@ +angular.module("admin.variantOverrides").filter "inventoryVariants", ($filter, InventoryItems) -> + return (variants, hub_id, views) -> + return [] if !hub_id + return $filter('filter')(variants, (variant) -> + if InventoryItems.inventoryItems.hasOwnProperty(hub_id) && InventoryItems.inventoryItems[hub_id].hasOwnProperty(variant.id) + if InventoryItems.inventoryItems[hub_id][variant.id].visible + return views.inventory.visible + else + return views.hidden.visible + else + return views.new.visible + , true) diff --git a/app/assets/javascripts/admin/variant_overrides/filters/new_inventory_products_filter.js.coffee b/app/assets/javascripts/admin/variant_overrides/filters/new_inventory_products_filter.js.coffee new file mode 100644 index 0000000000..fcc6f395d8 --- /dev/null +++ b/app/assets/javascripts/admin/variant_overrides/filters/new_inventory_products_filter.js.coffee @@ -0,0 +1,9 @@ +angular.module("admin.variantOverrides").filter "newInventoryProducts", ($filter, InventoryItems) -> + return (products, hub_id) -> + return [] if !hub_id + return products unless InventoryItems.inventoryItems.hasOwnProperty(hub_id) + return $filter('filter')(products, (product) -> + for variant in product.variants + return true if !InventoryItems.inventoryItems[hub_id].hasOwnProperty(variant.id) + false + , true) diff --git a/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee b/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee index ae46cd14c7..c302c0463e 100644 --- a/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee +++ b/app/assets/javascripts/admin/variant_overrides/variant_overrides.js.coffee @@ -1 +1 @@ -angular.module("admin.variantOverrides", ["pasvaz.bindonce", "admin.indexUtils", "admin.utils", "admin.dropdown"]) +angular.module("admin.variantOverrides", ["pasvaz.bindonce", "admin.indexUtils", "admin.utils", "admin.dropdown", "admin.inventoryItems"]) diff --git a/app/assets/javascripts/templates/admin/alert_row.html.haml b/app/assets/javascripts/templates/admin/alert_row.html.haml new file mode 100644 index 0000000000..c1dfe45c6f --- /dev/null +++ b/app/assets/javascripts/templates/admin/alert_row.html.haml @@ -0,0 +1,8 @@ +.sixteen.columns.alpha.omega.alert-row{ ng: { show: '!dismissed' } } + .fifteen.columns.pad.alpha + %span.message.text-big{ ng: { bind: 'message'} } +     + %input{ type: 'button', ng: { value: "buttonText", show: 'buttonText && buttonAction', click: "buttonAction()" } } + .one.column.omega.pad.text-center + %a.close{ href: "#", ng: { click: "dismiss()" } } + × diff --git a/app/assets/stylesheets/admin/advanced_settings.css.scss b/app/assets/stylesheets/admin/advanced_settings.css.scss new file mode 100644 index 0000000000..6b48e8ce11 --- /dev/null +++ b/app/assets/stylesheets/admin/advanced_settings.css.scss @@ -0,0 +1,19 @@ +#advanced_settings { + background-color: #eff5fc; + border: 1px solid #cee1f4; + margin-bottom: 20px; + + .row{ + margin: 0px -4px; + + padding: 20px 0px; + + .column.alpha, .columns.alpha { + padding-left: 20px; + } + + .column.omega, .columns.omega { + padding-right: 20px; + } + } +} diff --git a/app/assets/stylesheets/admin/components/alert_row.css.scss b/app/assets/stylesheets/admin/components/alert_row.css.scss new file mode 100644 index 0000000000..4c74afc56f --- /dev/null +++ b/app/assets/stylesheets/admin/components/alert_row.css.scss @@ -0,0 +1,21 @@ +.alert-row{ + margin-bottom: 10px; + font-weight: bold; + background-color: #eff5fc; + + .column, .columns { + padding-top: 8px; + padding-bottom: 8px; + &.alpha { padding-left: 10px; } + &.omega { padding-right: 10px; } + } + + span { + line-height: 3rem; + } + + a.close { + line-height: 3rem; + font-size: 2.0rem; + } +} diff --git a/app/assets/stylesheets/admin/dashboard_item.css.sass b/app/assets/stylesheets/admin/dashboard_item.css.sass index 13b16084f6..4bb4855660 100644 --- a/app/assets/stylesheets/admin/dashboard_item.css.sass +++ b/app/assets/stylesheets/admin/dashboard_item.css.sass @@ -25,7 +25,7 @@ div.dashboard_item border: 1px solid #5498da position: relative - a.with-tip + a[ofn-with-tip] position: absolute right: 5px bottom: 5px @@ -35,7 +35,7 @@ div.dashboard_item border-width: 3px h3 color: #DA5354 - + &.orange border-color: #DA7F52 border-width: 3px diff --git a/app/assets/stylesheets/admin/dropdown.css.scss b/app/assets/stylesheets/admin/dropdown.css.scss index 2dfa369c87..b848bc3c2c 100644 --- a/app/assets/stylesheets/admin/dropdown.css.scss +++ b/app/assets/stylesheets/admin/dropdown.css.scss @@ -27,9 +27,12 @@ -ms-user-select: none; user-select: none; text-align: center; + margin-right: 10px; &.right { float: right; + margin-right: 0px; + margin-left: 10px; } &:hover, &.expanded { @@ -55,12 +58,35 @@ background-color: #ffffff; box-shadow: 1px 3px 10px #888888; z-index: 100; + white-space: nowrap; + .menu_item { margin: 0px; - padding: 2px 0px; + padding: 2px 10px; color: #454545; text-align: left; + display: block; + + .check { + display: inline-block; + text-align: center; + width: 40px; + &:before { + content: "\00a0"; + } + } + + .name { + display: inline-block; + padding: 0px 15px 0px 0px; + } + + &.selected{ + .check:before { + content: "\2713"; + } + } } .menu_item:hover { diff --git a/app/assets/stylesheets/admin/offsets.css.scss b/app/assets/stylesheets/admin/offsets.css.scss new file mode 100644 index 0000000000..762b7469f6 --- /dev/null +++ b/app/assets/stylesheets/admin/offsets.css.scss @@ -0,0 +1,7 @@ +.margin-bottom-20 { + margin-bottom: 20px; +} + +.margin-bottom-50 { + margin-bottom: 50px; +} diff --git a/app/assets/stylesheets/admin/openfoodnetwork.css.scss b/app/assets/stylesheets/admin/openfoodnetwork.css.scss index e0916b4e79..17d43ff3a5 100644 --- a/app/assets/stylesheets/admin/openfoodnetwork.css.scss +++ b/app/assets/stylesheets/admin/openfoodnetwork.css.scss @@ -166,11 +166,16 @@ table#listing_enterprise_groups { } } +// TODO: remove this, use class below #no_results { font-weight:bold; color: #DA5354; } +.no-results { + font-weight:bold; + color: #DA5354; +} #loading { text-align: center; diff --git a/app/assets/stylesheets/admin/select2.css.scss b/app/assets/stylesheets/admin/select2.css.scss new file mode 100644 index 0000000000..daba11d099 --- /dev/null +++ b/app/assets/stylesheets/admin/select2.css.scss @@ -0,0 +1,10 @@ +.select2-container { + .select2-choice { + .select2-arrow { + width: 22px; + border: none; + background-image: none; + background-color: transparent; + } + } +} diff --git a/app/assets/stylesheets/admin/typography.css.scss b/app/assets/stylesheets/admin/typography.css.scss new file mode 100644 index 0000000000..20148df3f1 --- /dev/null +++ b/app/assets/stylesheets/admin/typography.css.scss @@ -0,0 +1,9 @@ +.text-normal { + font-size: 1.0rem; + font-weight: 300; +} + +.text-big { + font-size: 1.2rem; + font-weight: 300; +} diff --git a/app/assets/stylesheets/admin/variant_overrides.css.sass b/app/assets/stylesheets/admin/variant_overrides.css.sass index c0f51658b8..0488c1d56b 100644 --- a/app/assets/stylesheets/admin/variant_overrides.css.sass +++ b/app/assets/stylesheets/admin/variant_overrides.css.sass @@ -1,3 +1,6 @@ .variant-override-unit float: right font-style: italic + +button.hide:hover + background-color: #DA5354 diff --git a/app/controllers/admin/enterprise_fees_controller.rb b/app/controllers/admin/enterprise_fees_controller.rb index 866c05ea54..d966a6ad8e 100644 --- a/app/controllers/admin/enterprise_fees_controller.rb +++ b/app/controllers/admin/enterprise_fees_controller.rb @@ -16,18 +16,15 @@ module Admin respond_to do |format| format.html - format.json { @presented_collection = @collection.each_with_index.map { |ef, i| EnterpriseFeePresenter.new(self, ef, i) } } + format.json { render_as_json @collection, controller: self, include_calculators: @include_calculators } + # format.json { @presented_collection = @collection.each_with_index.map { |ef, i| EnterpriseFeePresenter.new(self, ef, i) } } 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 + format.json { render_as_json @collection, controller: self } end end diff --git a/app/controllers/admin/enterprises_controller.rb b/app/controllers/admin/enterprises_controller.rb index 13d5772385..55eaabd7d8 100644 --- a/app/controllers/admin/enterprises_controller.rb +++ b/app/controllers/admin/enterprises_controller.rb @@ -96,7 +96,7 @@ module Admin def for_order_cycle respond_to do |format| format.json do - render json: @collection, each_serializer: Api::Admin::ForOrderCycle::EnterpriseSerializer, spree_current_user: spree_current_user + render json: @collection, each_serializer: Api::Admin::ForOrderCycle::EnterpriseSerializer, order_cycle: @order_cycle, spree_current_user: spree_current_user end end end @@ -138,10 +138,10 @@ module Admin def collection case action when :for_order_cycle - order_cycle = OrderCycle.find_by_id(params[:order_cycle_id]) if params[:order_cycle_id] + @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 + @order_cycle = OrderCycle.new(coordinator: coordinator) if @order_cycle.nil? && coordinator.present? + return OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user, @order_cycle).visible_enterprises when :index if spree_current_user.admin? OpenFoodNetwork::Permissions.new(spree_current_user). diff --git a/app/controllers/admin/inventory_items_controller.rb b/app/controllers/admin/inventory_items_controller.rb new file mode 100644 index 0000000000..432b955134 --- /dev/null +++ b/app/controllers/admin/inventory_items_controller.rb @@ -0,0 +1,28 @@ +module Admin + class InventoryItemsController < ResourceController + + respond_to :json + + respond_override update: { json: { + success: lambda { render_as_json @inventory_item }, + failure: lambda { render json: { errors: @inventory_item.errors.full_messages }, status: :unprocessable_entity } + } } + + respond_override create: { json: { + success: lambda { render_as_json @inventory_item }, + failure: lambda { render json: { errors: @inventory_item.errors.full_messages }, status: :unprocessable_entity } + } } + + private + + # Overriding Spree method to load data from params here so that + # we can authorise #create using an object with required attributes + def build_resource + if parent_data.present? + parent.send(controller_name).build + else + model_class.new(params[object_name]) # This line changed + end + end + end +end diff --git a/app/controllers/admin/order_cycles_controller.rb b/app/controllers/admin/order_cycles_controller.rb index 300a2ca24e..1d5d018522 100644 --- a/app/controllers/admin/order_cycles_controller.rb +++ b/app/controllers/admin/order_cycles_controller.rb @@ -60,8 +60,12 @@ module Admin respond_to do |format| if @order_cycle.update_attributes(params[:order_cycle]) - OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, spree_current_user).go! + unless params[:order_cycle][:incoming_exchanges].nil? && params[:order_cycle][:outgoing_exchanges].nil? + # Only update apply exchange information if it is actually submmitted + OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, spree_current_user).go! + end flash[:notice] = 'Your order cycle has been updated.' if params[:reloading] == '1' + format.html { redirect_to main_app.edit_admin_order_cycle_path(@order_cycle) } format.json { render :json => {:success => true} } else format.json { render :json => {:success => false} } diff --git a/app/controllers/admin/variant_overrides_controller.rb b/app/controllers/admin/variant_overrides_controller.rb index c886f80652..446ca457eb 100644 --- a/app/controllers/admin/variant_overrides_controller.rb +++ b/app/controllers/admin/variant_overrides_controller.rb @@ -54,6 +54,8 @@ module Admin @hub_permissions = OpenFoodNetwork::Permissions.new(spree_current_user). variant_override_enterprises_per_hub + + @inventory_items = InventoryItem.where(enterprise_id: @hubs) end def load_collection diff --git a/app/controllers/discourse_sso_controller.rb b/app/controllers/discourse_sso_controller.rb new file mode 100644 index 0000000000..0b067f6c92 --- /dev/null +++ b/app/controllers/discourse_sso_controller.rb @@ -0,0 +1,55 @@ +require 'discourse/single_sign_on' + +class DiscourseSsoController < ApplicationController + include SharedHelper + include DiscourseHelper + + before_filter :require_config + + def login + if require_activation? + redirect_to discourse_url + else + redirect_to discourse_login_url + end + end + + def sso + if spree_current_user + begin + redirect_to sso_url + rescue TypeError + render text: "Bad SingleSignOn request.", status: :bad_request + end + else + redirect_to login_path + end + end + + private + + def sso_url + secret = discourse_sso_secret! + discourse_url = discourse_url! + sso = Discourse::SingleSignOn.parse(request.query_string, secret) + sso.email = spree_current_user.email + sso.username = spree_current_user.login + sso.external_id = spree_current_user.id + sso.sso_secret = secret + sso.admin = admin_user? + sso.require_activation = require_activation? + sso.to_url(discourse_sso_url) + end + + def require_config + raise ActionController::RoutingError.new('Not Found') unless discourse_configured? + end + + def require_activation? + !admin_user? && !email_validated? + end + + def email_validated? + spree_current_user.enterprises.confirmed.map(&:email).include?(spree_current_user.email) + end +end diff --git a/app/controllers/shop_controller.rb b/app/controllers/shop_controller.rb index ef78605b41..8d7b9ab274 100644 --- a/app/controllers/shop_controller.rb +++ b/app/controllers/shop_controller.rb @@ -1,4 +1,4 @@ -require 'open_food_network/scope_product_to_hub' +require 'open_food_network/products_renderer' class ShopController < BaseController layout "darkswarm" @@ -10,22 +10,13 @@ class ShopController < BaseController end def products - if @products = products_for_shop + begin + products_json = OpenFoodNetwork::ProductsRenderer.new(current_distributor, current_order_cycle).products - enterprise_fee_calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new current_distributor, current_order_cycle + render json: products_json - render status: 200, - json: ActiveModel::ArraySerializer.new(@products, - each_serializer: Api::ProductSerializer, - current_order_cycle: current_order_cycle, - current_distributor: current_distributor, - variants: variants_for_shop_by_id, - master_variants: master_variants_for_shop_by_id, - enterprise_fee_calculator: enterprise_fee_calculator, - ).to_json - - else - render json: "", status: 404 + rescue OpenFoodNetwork::ProductsRenderer::NoProducts + render status: 404, json: '' end end @@ -42,55 +33,4 @@ class ShopController < BaseController end end - private - - def products_for_shop - if current_order_cycle - scoper = OpenFoodNetwork::ScopeProductToHub.new(current_distributor) - - current_order_cycle. - valid_products_distributed_by(current_distributor). - order(taxon_order). - each { |p| scoper.scope(p) }. - select { |p| !p.deleted? && p.has_stock_for_distribution?(current_order_cycle, current_distributor) } - end - end - - def taxon_order - if current_distributor.preferred_shopfront_taxon_order.present? - current_distributor - .preferred_shopfront_taxon_order - .split(",").map { |id| "primary_taxon_id=#{id} DESC" } - .join(",") + ", name ASC" - else - "name ASC" - end - end - - def all_variants_for_shop - # We use the in_stock? method here instead of the in_stock scope because we need to - # look up the stock as overridden by VariantOverrides, and the scope method is not affected - # by them. - scoper = OpenFoodNetwork::ScopeVariantToHub.new(current_distributor) - Spree::Variant. - for_distribution(current_order_cycle, current_distributor). - each { |v| scoper.scope(v) }. - select(&:in_stock?) - end - - def variants_for_shop_by_id - index_by_product_id all_variants_for_shop.reject(&:is_master) - end - - def master_variants_for_shop_by_id - index_by_product_id all_variants_for_shop.select(&:is_master) - end - - def index_by_product_id(variants) - variants.inject({}) do |vs, v| - vs[v.product_id] ||= [] - vs[v.product_id] << v - vs - end - end end diff --git a/app/helpers/admin/injection_helper.rb b/app/helpers/admin/injection_helper.rb index 343e15ef29..511eced707 100644 --- a/app/helpers/admin/injection_helper.rb +++ b/app/helpers/admin/injection_helper.rb @@ -39,6 +39,10 @@ module Admin admin_inject_json_ams_array opts[:module], "producers", @producers, Api::Admin::IdNameSerializer end + def admin_inject_inventory_items(opts={module: 'ofn.admin'}) + admin_inject_json_ams_array opts[:module], "inventoryItems", @inventory_items, Api::Admin::InventoryItemSerializer + end + def admin_inject_enterprise_permissions permissions = {can_manage_shipping_methods: can?(:manage_shipping_methods, @enterprise), @@ -56,8 +60,8 @@ module Admin admin_inject_json_ams_array "ofn.admin", "products", @products, Api::Admin::ProductSerializer end - def admin_inject_tax_categories - admin_inject_json_ams_array "ofn.admin", "tax_categories", @tax_categories, Api::Admin::TaxCategorySerializer + def admin_inject_tax_categories(opts={module: 'ofn.admin'}) + admin_inject_json_ams_array opts[:module], "tax_categories", @tax_categories, Api::Admin::TaxCategorySerializer end def admin_inject_taxons diff --git a/app/helpers/angular_form_builder.rb b/app/helpers/angular_form_builder.rb index 56fcab79fa..4dff1f92e1 100644 --- a/app/helpers/angular_form_builder.rb +++ b/app/helpers/angular_form_builder.rb @@ -11,26 +11,26 @@ class AngularFormBuilder < ActionView::Helpers::FormBuilder # @fields_for_record_name --> :collection # @object.send(@fields_for_record_name).first.class.to_s.underscore --> enterprise_fee - value = "{{ #{@object.send(@fields_for_record_name).first.class.to_s.underscore}.#{method} }}" + value = "{{ #{angular_model(method)} }}" options.reverse_merge!({'id' => angular_id(method)}) @template.text_field_tag angular_name(method), value, options end def ng_hidden_field(method, options = {}) - value = "{{ #{@object.send(@fields_for_record_name).first.class.to_s.underscore}.#{method} }}" + value = "{{ #{angular_model(method)} }}" @template.hidden_field_tag angular_name(method), value, :id => angular_id(method) end def ng_select(method, choices, angular_field, options = {}) - options.reverse_merge!({'id' => angular_id(method)}) + options.reverse_merge!({'id' => angular_id(method), 'ng-model' => "#{angular_model(method)}"}) @template.select_tag angular_name(method), @template.ng_options_for_select(choices, angular_field), options end def ng_collection_select(method, collection, value_method, text_method, angular_field, options = {}) - options.reverse_merge!({'id' => angular_id(method)}) + options.reverse_merge!({'id' => angular_id(method), 'ng-model' => "#{angular_model(method)}"}) @template.select_tag angular_name(method), @template.ng_options_from_collection_for_select(collection, value_method, text_method, angular_field), options end @@ -43,4 +43,8 @@ class AngularFormBuilder < ActionView::Helpers::FormBuilder def angular_id(method) "#{@object_name}_#{@fields_for_record_name}_attributes_{{ $index }}_#{method}" end + + def angular_model(method) + "#{@object.send(@fields_for_record_name).first.class.to_s.underscore}.#{method}" + end end diff --git a/app/helpers/angular_form_helper.rb b/app/helpers/angular_form_helper.rb index a7fd6e0e28..0687f62188 100644 --- a/app/helpers/angular_form_helper.rb +++ b/app/helpers/angular_form_helper.rb @@ -5,8 +5,7 @@ module AngularFormHelper container.map do |element| html_attributes = option_html_attributes(element) text, value = option_text_and_value(element).map { |item| item.to_s } - selected_attribute = %Q( ng-selected="#{angular_field} == '#{value}'") if angular_field - %() + %() end.join("\n").html_safe end diff --git a/app/helpers/discourse_helper.rb b/app/helpers/discourse_helper.rb new file mode 100644 index 0000000000..1d813cf404 --- /dev/null +++ b/app/helpers/discourse_helper.rb @@ -0,0 +1,25 @@ +module DiscourseHelper + def discourse_configured? + discourse_url.present? + end + + def discourse_url + ENV['DISCOURSE_URL'] + end + + def discourse_login_url + discourse_url + '/login' + end + + def discourse_sso_url + discourse_url + '/session/sso_login' + end + + def discourse_url! + discourse_url or raise 'Missing Discourse URL' + end + + def discourse_sso_secret! + ENV['DISCOURSE_SSO_SECRET'] or raise 'Missing SSO secret' + end +end diff --git a/app/helpers/enterprise_fees_helper.rb b/app/helpers/enterprise_fees_helper.rb index b7ec2b9018..82d24ce1a6 100644 --- a/app/helpers/enterprise_fees_helper.rb +++ b/app/helpers/enterprise_fees_helper.rb @@ -1,4 +1,12 @@ module EnterpriseFeesHelper + def angular_name(method) + "enterprise_fee_set[collection_attributes][{{ $index }}][#{method}]" + end + + def angular_id(method) + "enterprise_fee_set_collection_attributes_{{ $index }}_#{method}" + end + def enterprise_fee_type_options EnterpriseFee::FEE_TYPES.map { |f| [f.capitalize, f] } end diff --git a/app/helpers/spree/admin/base_helper_decorator.rb b/app/helpers/spree/admin/base_helper_decorator.rb index 82bf794073..86e77431ba 100644 --- a/app/helpers/spree/admin/base_helper_decorator.rb +++ b/app/helpers/spree/admin/base_helper_decorator.rb @@ -5,7 +5,7 @@ module Spree def link_to_remove_fields(name, f, options = {}) name = '' if options[:no_text] options[:class] = '' unless options[:class] - options[:class] += 'no-text with-tip' if options[:no_text] + options[:class] += 'no-text' if options[:no_text] url = if f.object.persisted? options[:url] || [:admin, f.object] diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 34a5d22f5b..bc46cbd58f 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -8,6 +8,11 @@ class Enterprise < ActiveRecord::Base preference :shopfront_taxon_order, :string, default: "" preference :shopfront_order_cycle_order, :string, default: "orders_close_at" + # This is hopefully a temporary measure, pending the arrival of multiple named inventories + # for shops. We need this here to allow hubs to restrict visible variants to only those in + # their inventory if they so choose + preference :product_selection_from_inventory_only, :boolean, default: false + devise :confirmable, reconfirmable: true, confirmation_keys: [ :id, :email ] handle_asynchronously :send_confirmation_instructions handle_asynchronously :send_on_create_confirmation_instructions @@ -36,6 +41,7 @@ class Enterprise < ActiveRecord::Base has_many :shipping_methods, through: :distributor_shipping_methods has_many :customers has_many :billable_periods + has_many :inventory_items delegate :latitude, :longitude, :city, :state_name, :to => :address @@ -165,7 +171,7 @@ class Enterprise < ActiveRecord::Base if user.has_spree_role?('admin') scoped else - joins(:enterprise_roles).where('enterprise_roles.user_id = ?', user.id).select("DISTINCT enterprises.*") + joins(:enterprise_roles).where('enterprise_roles.user_id = ?', user.id) end } @@ -247,6 +253,14 @@ class Enterprise < ActiveRecord::Base strip_url read_attribute(:linkedin) end + def inventory_variants + if prefers_product_selection_from_inventory_only? + Spree::Variant.visible_for(self) + else + Spree::Variant.not_hidden_for(self) + end + end + def distributed_variants Spree::Variant.joins(:product).merge(Spree::Product.in_distributor(self)).select('spree_variants.*') end diff --git a/app/models/enterprise_fee.rb b/app/models/enterprise_fee.rb index e19f91d0f4..59b2648d30 100644 --- a/app/models/enterprise_fee.rb +++ b/app/models/enterprise_fee.rb @@ -9,7 +9,7 @@ class EnterpriseFee < ActiveRecord::Base calculated_adjustments - attr_accessible :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type + attr_accessible :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type, :inherits_tax_category FEE_TYPES = %w(packing transport admin sales fundraising) PER_ORDER_CALCULATORS = ['Spree::Calculator::FlatRate', 'Spree::Calculator::FlexiRate'] @@ -18,6 +18,7 @@ class EnterpriseFee < ActiveRecord::Base validates_inclusion_of :fee_type, :in => FEE_TYPES validates_presence_of :name + before_save :ensure_valid_tax_category_settings scope :for_enterprise, lambda { |enterprise| where(enterprise_id: enterprise) } scope :for_enterprises, lambda { |enterprises| where(enterprise_id: enterprises) } @@ -57,4 +58,18 @@ class EnterpriseFee < ActiveRecord::Base :mandatory => mandatory, :locked => true}, :without_protection => true) end + + private + + def ensure_valid_tax_category_settings + # Setting an explicit tax_category removes any inheritance behaviour + # In the absence of any current changes to tax_category, setting + # inherits_tax_category to true will clear the tax_category + if tax_category_id_changed? + self.inherits_tax_category = false if tax_category.present? + elsif inherits_tax_category_changed? + self.tax_category_id = nil if inherits_tax_category? + end + return true + end end diff --git a/app/models/enterprise_relationship.rb b/app/models/enterprise_relationship.rb index 8d279b2a20..da0996fdc5 100644 --- a/app/models/enterprise_relationship.rb +++ b/app/models/enterprise_relationship.rb @@ -6,6 +6,8 @@ class EnterpriseRelationship < ActiveRecord::Base validates_presence_of :parent_id, :child_id validates_uniqueness_of :child_id, scope: :parent_id, message: "^That relationship is already established." + after_save :apply_variant_override_permissions + scope :with_enterprises, joins('LEFT JOIN enterprises AS parent_enterprises ON parent_enterprises.id = enterprise_relationships.parent_id'). joins('LEFT JOIN enterprises AS child_enterprises ON child_enterprises.id = enterprise_relationships.child_id') @@ -61,10 +63,28 @@ class EnterpriseRelationship < ActiveRecord::Base def permissions_list=(perms) - perms.andand.each { |name| permissions.build name: name } + if perms.nil? + permissions.destroy_all + else + permissions.where('name NOT IN (?)', perms).destroy_all + perms.map { |name| permissions.find_or_initialize_by_name name } + end end def has_permission?(name) - permissions.map(&:name).map(&:to_sym).include? name.to_sym + permissions(:reload).map(&:name).map(&:to_sym).include? name.to_sym + end + + private + + def apply_variant_override_permissions + variant_overrides = VariantOverride.unscoped.for_hubs(child) + .joins(variant: :product).where("spree_products.supplier_id IN (?)", parent) + + if has_permission?(:create_variant_overrides) + variant_overrides.update_all(permission_revoked_at: nil) + else + variant_overrides.update_all(permission_revoked_at: Time.now) + end end end diff --git a/app/models/inventory_item.rb b/app/models/inventory_item.rb new file mode 100644 index 0000000000..2648e38341 --- /dev/null +++ b/app/models/inventory_item.rb @@ -0,0 +1,14 @@ +class InventoryItem < ActiveRecord::Base + attr_accessible :enterprise, :enterprise_id, :variant, :variant_id, :visible + + belongs_to :enterprise + belongs_to :variant, class_name: "Spree::Variant" + + validates :variant_id, uniqueness: { scope: :enterprise_id } + validates :enterprise_id, presence: true + validates :variant_id, presence: true + validates :visible, inclusion: { in: [true, false], message: "must be true or false" } + + scope :visible, where(visible: true) + scope :hidden, where(visible: false) +end diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index 03979220d6..31a68faa37 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -11,6 +11,8 @@ class OrderCycle < ActiveRecord::Base validates_presence_of :name, :coordinator_id + preference :product_selection_from_coordinator_inventory_only, :boolean, default: false + scope :active, lambda { where('order_cycles.orders_open_at <= ? AND order_cycles.orders_close_at >= ?', Time.zone.now, Time.zone.now) } scope :active_or_complete, lambda { where('order_cycles.orders_open_at <= ?', Time.zone.now) } scope :inactive, lambda { where('order_cycles.orders_open_at > ? OR order_cycles.orders_close_at < ?', Time.zone.now, Time.zone.now) } @@ -113,6 +115,7 @@ class OrderCycle < ActiveRecord::Base oc.name = "COPY OF #{oc.name}" oc.orders_open_at = oc.orders_close_at = nil oc.coordinator_fee_ids = self.coordinator_fee_ids + oc.preferred_product_selection_from_coordinator_inventory_only = self.preferred_product_selection_from_coordinator_inventory_only oc.save! self.exchanges.each { |e| e.clone!(oc) } oc.reload @@ -142,12 +145,15 @@ class OrderCycle < ActiveRecord::Base end def distributed_variants + # TODO: only used in DistributionChangeValidator, can we remove? self.exchanges.outgoing.map(&:variants).flatten.uniq.reject(&:deleted?) end def variants_distributed_by(distributor) + return Spree::Variant.where("1=0") unless distributor.present? Spree::Variant. not_deleted. + merge(distributor.inventory_variants). joins(:exchanges). merge(Exchange.in_order_cycle(self)). merge(Exchange.outgoing). diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index 9bc303116b..8c93fe4b71 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -125,6 +125,20 @@ class AbilityDecorator hub_auth && producer_auth end + can [:admin, :create, :update], InventoryItem do |ii| + next false unless ii.enterprise.present? && ii.variant.andand.product.andand.supplier.present? + + hub_auth = OpenFoodNetwork::Permissions.new(user). + variant_override_hubs. + include? ii.enterprise + + producer_auth = OpenFoodNetwork::Permissions.new(user). + variant_override_producers. + include? ii.variant.product.supplier + + hub_auth && producer_auth + end + can [:admin, :index, :read, :create, :edit, :update_positions, :destroy], Spree::ProductProperty can [:admin, :index, :read, :create, :edit, :update, :destroy], Spree::Image diff --git a/app/models/spree/calculator/default_tax_decorator.rb b/app/models/spree/calculator/default_tax_decorator.rb new file mode 100644 index 0000000000..623f2df26d --- /dev/null +++ b/app/models/spree/calculator/default_tax_decorator.rb @@ -0,0 +1,40 @@ +require 'open_food_network/enterprise_fee_calculator' + +Spree::Calculator::DefaultTax.class_eval do + + private + + # Override this method to enable calculation of tax for + # enterprise fees with tax rates where included_in_price = false + def compute_order(order) + matched_line_items = order.line_items.select do |line_item| + line_item.product.tax_category == rate.tax_category + end + + line_items_total = matched_line_items.sum(&:total) + + # Added this line + calculator = OpenFoodNetwork::EnterpriseFeeCalculator.new(order.distributor, order.order_cycle) + + # Added this block, finds relevant fees for each line_item, calculates the tax on them, and returns the total tax + per_item_fees_total = order.line_items.sum do |line_item| + calculator.send(:per_item_enterprise_fee_applicators_for, line_item.variant) + .select { |applicator| (!applicator.enterprise_fee.inherits_tax_category && applicator.enterprise_fee.tax_category == rate.tax_category) || + (applicator.enterprise_fee.inherits_tax_category && line_item.product.tax_category == rate.tax_category) } + .sum { |applicator| applicator.enterprise_fee.compute_amount(line_item) } + end + + # Added this block, finds relevant fees for whole order, calculates the tax on them, and returns the total tax + per_order_fees_total = calculator.send(:per_order_enterprise_fee_applicators_for, order) + .select { |applicator| applicator.enterprise_fee.tax_category == rate.tax_category } + .sum { |applicator| applicator.enterprise_fee.compute_amount(order) } + + # round_to_two_places(line_items_total * rate.amount) # Removed this line + + # Added this block + [line_items_total, per_item_fees_total, per_order_fees_total].sum do |total| + round_to_two_places(total * rate.amount) + end + end + +end diff --git a/app/models/spree/product_decorator.rb b/app/models/spree/product_decorator.rb index b089c9a52b..c7e1cfb68d 100644 --- a/app/models/spree/product_decorator.rb +++ b/app/models/spree/product_decorator.rb @@ -52,6 +52,13 @@ Spree::Product.class_eval do scope :with_order_cycles_inner, joins(:variants_including_master => {:exchanges => :order_cycle}) + scope :visible_for, lambda { |enterprise| + joins('LEFT OUTER JOIN spree_variants AS o_spree_variants ON (o_spree_variants.product_id = spree_products.id)'). + joins('LEFT OUTER JOIN inventory_items AS o_inventory_items ON (o_spree_variants.id = o_inventory_items.variant_id)'). + where('o_inventory_items.enterprise_id = (?) AND visible = (?)', enterprise, true). + select('DISTINCT spree_products.*') + } + # -- Scopes scope :in_supplier, lambda { |supplier| where(:supplier_id => supplier) } diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb index 33e7f5bffd..ea04a1a5ac 100644 --- a/app/models/spree/variant_decorator.rb +++ b/app/models/spree/variant_decorator.rb @@ -12,6 +12,7 @@ Spree::Variant.class_eval do has_many :exchange_variants, dependent: :destroy has_many :exchanges, through: :exchange_variants has_many :variant_overrides + has_many :inventory_items attr_accessible :unit_value, :unit_description, :images_attributes, :display_as, :display_name accepts_nested_attributes_for :images @@ -40,6 +41,16 @@ Spree::Variant.class_eval do where('spree_variants.id IN (?)', order_cycle.variants_distributed_by(distributor)) } + scope :visible_for, lambda { |enterprise| + joins(:inventory_items).where('inventory_items.enterprise_id = (?) AND inventory_items.visible = (?)', enterprise, true) + } + + scope :not_hidden_for, lambda { |enterprise| + return where("1=0") unless enterprise.present? + joins("LEFT OUTER JOIN (SELECT * from inventory_items WHERE enterprise_id = #{sanitize enterprise.andand.id}) AS o_inventory_items ON o_inventory_items.variant_id = spree_variants.id") + .where("o_inventory_items.id IS NULL OR o_inventory_items.visible = (?)", true) + } + # Define sope as class method to allow chaining with other scopes filtering id. # In Rails 3, merging two scopes on the same column will consider only the last scope. def self.in_distributor(distributor) diff --git a/app/models/variant_override.rb b/app/models/variant_override.rb index 21820ce0db..baede3e62d 100644 --- a/app/models/variant_override.rb +++ b/app/models/variant_override.rb @@ -6,6 +6,8 @@ class VariantOverride < ActiveRecord::Base # Default stock can be nil, indicating stock should not be reset or zero, meaning reset to zero. Need to ensure this can be set by the user. validates :default_stock, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true + default_scope where(permission_revoked_at: nil) + scope :for_hubs, lambda { |hubs| where(hub_id: hubs) } diff --git a/app/overrides/spree/admin/image_settings/edit/add_image_format.html.haml.deface b/app/overrides/spree/admin/image_settings/edit/add_image_format.html.haml.deface index deb79b5bbf..fe050c6c7c 100644 --- a/app/overrides/spree/admin/image_settings/edit/add_image_format.html.haml.deface +++ b/app/overrides/spree/admin/image_settings/edit/add_image_format.html.haml.deface @@ -3,7 +3,7 @@ - @styles.each_with_index do |(style_name, style_value), index| .field.three.columns = label_tag "attachment_styles[#{style_name}]", style_name - %a.destroy_style.with-tip{:alt => t(:destroy), :href => "#", :title => t(:destroy)} + %a.destroy_style{:alt => t(:destroy), :href => "#", :title => t(:destroy)} %i.icon-trash = text_field_tag "attachment_styles[#{style_name}][]", admin_image_settings_geometry_from_style(style_value), :class => 'fullwidth' %br/ diff --git a/app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface b/app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface index 75a2bc4689..1711343f3c 100644 --- a/app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface +++ b/app/overrides/spree/admin/orders/index/add_special_instructions.html.haml.deface @@ -2,5 +2,5 @@ - if order.special_instructions.present? %br - %span{class: "icon-warning-sign with-tip", title: order.special_instructions} + %span{class: "icon-warning-sign", "ofn-with-tip" => order.special_instructions} notes diff --git a/app/overrides/spree/admin/orders/index/set_ng_app.deface b/app/overrides/spree/admin/orders/index/set_ng_app.deface new file mode 100644 index 0000000000..9ca071be11 --- /dev/null +++ b/app/overrides/spree/admin/orders/index/set_ng_app.deface @@ -0,0 +1,2 @@ +add_to_attributes "table#listing_orders" +attributes "ng-app" => "ofn.admin" diff --git a/app/overrides/spree/admin/shared/_head/replace_spree_title.html.haml.deface b/app/overrides/spree/admin/shared/_head/replace_spree_title.html.haml.deface index 4b9bd4142d..906a09bd8c 100644 --- a/app/overrides/spree/admin/shared/_head/replace_spree_title.html.haml.deface +++ b/app/overrides/spree/admin/shared/_head/replace_spree_title.html.haml.deface @@ -1,4 +1,7 @@ / replace_contents "title" -= t(controller.controller_name, :default => controller.controller_name.titleize) -= " - OFN #{t(:administration)}" \ No newline at end of file +- if content_for? :html_title + = yield :html_title +- else + = t(controller.controller_name, :default => controller.controller_name.titleize) += " - OFN #{t(:administration)}" diff --git a/app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface b/app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface index 8db108b4f2..f0e507234a 100644 --- a/app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface +++ b/app/overrides/spree/admin/shared/_product_sub_menu/add_variant_overrides_tab.html.haml.deface @@ -1,3 +1,3 @@ / insert_bottom "[data-hook='admin_product_sub_tabs']" -= tab :variant_overrides, label: "Overrides", url: main_app.admin_variant_overrides_path, match_path: '/variant_overrides' += tab :variant_overrides, label: "Inventory", url: main_app.admin_inventory_path, match_path: '/inventory' diff --git a/app/overrides/spree/layouts/admin/add_app_wrapper.html.erb.deface b/app/overrides/spree/layouts/admin/add_app_wrapper.html.erb.deface new file mode 100644 index 0000000000..9c5e6fcd80 --- /dev/null +++ b/app/overrides/spree/layouts/admin/add_app_wrapper.html.erb.deface @@ -0,0 +1,5 @@ + + +
> + <%= render_original %> +
diff --git a/app/presenters/enterprise_fee_presenter.rb b/app/presenters/enterprise_fee_presenter.rb deleted file mode 100644 index b8f9ae4655..0000000000 --- a/app/presenters/enterprise_fee_presenter.rb +++ /dev/null @@ -1,31 +0,0 @@ -class EnterpriseFeePresenter - def initialize(controller, enterprise_fee, index) - @controller, @enterprise_fee, @index = controller, enterprise_fee, index - end - - delegate :id, :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type, :to => :enterprise_fee - - def enterprise_fee - @enterprise_fee - end - - - def enterprise_name - @enterprise_fee.enterprise.andand.name - end - - def calculator_description - @enterprise_fee.calculator.andand.description - end - - def calculator_settings - result = nil - - @controller.send(:with_format, :html) do - result = @controller.render_to_string :partial => 'admin/enterprise_fees/calculator_settings', :locals => {:enterprise_fee => @enterprise_fee, :index => @index} - end - - result.gsub('[0]', '[{{ $index }}]').gsub('_0_', '_{{ $index }}_') - end - -end diff --git a/app/serializers/api/admin/calculator_serializer.rb b/app/serializers/api/admin/calculator_serializer.rb new file mode 100644 index 0000000000..a6287edbb1 --- /dev/null +++ b/app/serializers/api/admin/calculator_serializer.rb @@ -0,0 +1,11 @@ +class Api::Admin::CalculatorSerializer < ActiveModel::Serializer + attributes :name, :description + + def name + object.name + end + + def description + object.description + end +end diff --git a/app/serializers/api/admin/enterprise_fee_serializer.rb b/app/serializers/api/admin/enterprise_fee_serializer.rb index 1b5a201b65..96c279d1c0 100644 --- a/app/serializers/api/admin/enterprise_fee_serializer.rb +++ b/app/serializers/api/admin/enterprise_fee_serializer.rb @@ -1,5 +1,5 @@ class Api::Admin::EnterpriseFeeSerializer < ActiveModel::Serializer - attributes :id, :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type + attributes :id, :enterprise_id, :fee_type, :name, :tax_category_id, :inherits_tax_category, :calculator_type attributes :enterprise_name, :calculator_description, :calculator_settings def enterprise_name @@ -11,6 +11,8 @@ class Api::Admin::EnterpriseFeeSerializer < ActiveModel::Serializer end def calculator_settings + return nil unless options[:include_calculators] + result = nil options[:controller].send(:with_format, :html) do diff --git a/app/serializers/api/admin/enterprise_serializer.rb b/app/serializers/api/admin/enterprise_serializer.rb index 8f955bdae2..37a96b402f 100644 --- a/app/serializers/api/admin/enterprise_serializer.rb +++ b/app/serializers/api/admin/enterprise_serializer.rb @@ -2,6 +2,7 @@ class Api::Admin::EnterpriseSerializer < ActiveModel::Serializer attributes :name, :id, :is_primary_producer, :is_distributor, :sells, :category, :payment_method_ids, :shipping_method_ids attributes :producer_profile_only, :email, :long_description, :permalink attributes :preferred_shopfront_message, :preferred_shopfront_closed_message, :preferred_shopfront_taxon_order, :preferred_shopfront_order_cycle_order + attributes :preferred_product_selection_from_inventory_only attributes :owner, :users has_one :owner, serializer: Api::Admin::UserSerializer diff --git a/app/serializers/api/admin/exchange_serializer.rb b/app/serializers/api/admin/exchange_serializer.rb index cb49d94fc8..615d49f695 100644 --- a/app/serializers/api/admin/exchange_serializer.rb +++ b/app/serializers/api/admin/exchange_serializer.rb @@ -4,14 +4,35 @@ class Api::Admin::ExchangeSerializer < ActiveModel::Serializer 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) + variants = object.incoming? ? visible_incoming_variants : visible_outgoing_variants + Hash[ object.variants.merge(variants).map { |v| [v.id, true] } ] + end + + private + + def visible_incoming_variants + if object.order_cycle.prefers_product_selection_from_coordinator_inventory_only? + permitted_incoming_variants.visible_for(object.order_cycle.coordinator) else - permitted = OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object.order_cycle). - visible_variants_for_outgoing_exchanges_to(object.receiver) + permitted_incoming_variants end - Hash[ object.variants.merge(permitted).map { |v| [v.id, true] } ] + end + + def visible_outgoing_variants + if object.receiver.prefers_product_selection_from_inventory_only? + permitted_outgoing_variants.visible_for(object.receiver) + else + permitted_outgoing_variants.not_hidden_for(object.receiver) + end + end + + def permitted_incoming_variants + OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object.order_cycle). + visible_variants_for_incoming_exchanges_from(object.sender) + end + + def permitted_outgoing_variants + OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object.order_cycle) + .visible_variants_for_outgoing_exchanges_to(object.receiver) 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 index 9dfa680476..8e4dee5f5d 100644 --- a/app/serializers/api/admin/for_order_cycle/enterprise_serializer.rb +++ b/app/serializers/api/admin/for_order_cycle/enterprise_serializer.rb @@ -6,7 +6,11 @@ class Api::Admin::ForOrderCycle::EnterpriseSerializer < ActiveModel::Serializer attributes :is_primary_producer, :is_distributor, :sells def issues_summary_supplier - OpenFoodNetwork::EnterpriseIssueValidator.new(object).issues_summary confirmation_only: true + issues = OpenFoodNetwork::EnterpriseIssueValidator.new(object).issues_summary confirmation_only: true + if issues.nil? && products.empty? + issues = "no products in inventory" + end + issues end def issues_summary_distributor @@ -18,8 +22,22 @@ class Api::Admin::ForOrderCycle::EnterpriseSerializer < ActiveModel::Serializer end def supplied_products - objects = object.supplied_products.not_deleted serializer = Api::Admin::ForOrderCycle::SuppliedProductSerializer - ActiveModel::ArraySerializer.new(objects, each_serializer: serializer) + ActiveModel::ArraySerializer.new(products, each_serializer: serializer, order_cycle: order_cycle) + end + + private + + def products + return @products unless @products.nil? + @products = if order_cycle.prefers_product_selection_from_coordinator_inventory_only? + object.supplied_products.not_deleted.visible_for(order_cycle.coordinator) + else + object.supplied_products.not_deleted + end + end + + def order_cycle + options[:order_cycle] 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 index dbe3ec7a48..054fe44751 100644 --- a/app/serializers/api/admin/for_order_cycle/supplied_product_serializer.rb +++ b/app/serializers/api/admin/for_order_cycle/supplied_product_serializer.rb @@ -14,8 +14,17 @@ class Api::Admin::ForOrderCycle::SuppliedProductSerializer < ActiveModel::Serial end def variants - object.variants.map do |variant| - { id: variant.id, label: variant.full_name } + variants = if order_cycle.prefers_product_selection_from_coordinator_inventory_only? + object.variants.visible_for(order_cycle.coordinator) + else + object.variants end + variants.map { |variant| { id: variant.id, label: variant.full_name } } + end + + private + + def order_cycle + options[:order_cycle] end end diff --git a/app/serializers/api/admin/inventory_item_serializer.rb b/app/serializers/api/admin/inventory_item_serializer.rb new file mode 100644 index 0000000000..15f8d35058 --- /dev/null +++ b/app/serializers/api/admin/inventory_item_serializer.rb @@ -0,0 +1,3 @@ +class Api::Admin::InventoryItemSerializer < ActiveModel::Serializer + attributes :id, :enterprise_id, :variant_id, :visible +end diff --git a/app/serializers/api/admin/order_cycle_serializer.rb b/app/serializers/api/admin/order_cycle_serializer.rb index 53b1a10c04..8068055df2 100644 --- a/app/serializers/api/admin/order_cycle_serializer.rb +++ b/app/serializers/api/admin/order_cycle_serializer.rb @@ -58,7 +58,14 @@ class Api::Admin::OrderCycleSerializer < ActiveModel::Serializer 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) + # This is hopefully a temporary measure, pending the arrival of multiple named inventories + # for shops. We need this here to allow hubs to restrict visible variants to only those in + # their inventory if they so choose + variants = if enterprise.prefers_product_selection_from_inventory_only? + permissions.visible_variants_for_outgoing_exchanges_to(enterprise).visible_for(enterprise) + else + permissions.visible_variants_for_outgoing_exchanges_to(enterprise).not_hidden_for(enterprise) + end.pluck(:id) visible[enterprise.id] = variants if variants.any? end visible diff --git a/app/serializers/api/product_serializer.rb b/app/serializers/api/product_serializer.rb index 5a1d1b5c86..03a66afe89 100644 --- a/app/serializers/api/product_serializer.rb +++ b/app/serializers/api/product_serializer.rb @@ -34,6 +34,7 @@ end class Api::CachedProductSerializer < ActiveModel::Serializer #cached #delegate :cache_key, to: :object + include ActionView::Helpers::SanitizeHelper attributes :id, :name, :permalink, :count_on_hand attributes :on_demand, :group_buy, :notes, :description @@ -48,6 +49,10 @@ class Api::CachedProductSerializer < ActiveModel::Serializer has_many :images, serializer: Api::ImageSerializer has_one :supplier, serializer: Api::IdSerializer + def description + strip_tags object.description + end + def properties_with_values object.properties_including_inherited end diff --git a/app/views/admin/business_model_configuration/edit.html.haml b/app/views/admin/business_model_configuration/edit.html.haml index 09a5949456..89345178f9 100644 --- a/app/views/admin/business_model_configuration/edit.html.haml +++ b/app/views/admin/business_model_configuration/edit.html.haml @@ -1,12 +1,15 @@ = render :partial => 'spree/admin/shared/configuration_menu' +- content_for :app_wrapper_attrs do + = "ng-app='admin.businessModelConfiguration'" + - content_for :page_title do %h1.page-title= t(:business_model_configuration) - %a.with-tip{ 'data-powertip' => "Configure the rate at which shops will be charged each month for use of the Open Food Network." } What's this? + %a{ 'ofn-with-tip' => "Configure the rate at which shops will be charged each month for use of the Open Food Network." } What's this? = render 'spree/shared/error_messages', target: @settings -.row{ ng: { app: 'admin.businessModelConfiguration', controller: "BusinessModelConfigCtrl" } } +.row{ ng: { controller: "BusinessModelConfigCtrl" } } .five.columns.omega %fieldset.no-border-bottom %legend=t(:bill_calculation_settings) @@ -17,7 +20,7 @@ .row .three.columns.alpha = f.label :account_invoices_monthly_fixed, t(:fixed_monthly_charge) - %span.with-tip.icon-question-sign{'data-powertip' => "A fixed monthly charge for ALL enterprises who are set up as a shop, regardless of how much produce they sell."} + %span.icon-question-sign{'ofn-with-tip' => "A fixed monthly charge for ALL enterprises who are set up as a shop, regardless of how much produce they sell."} .two.columns.omega .input-symbol.before %span= Spree::Money.currency_symbol @@ -25,13 +28,13 @@ .row .three.columns.alpha = f.label :account_invoices_monthly_rate, t(:percentage_of_turnover) - %span.with-tip.icon-question-sign{'data-powertip' => "When greater than zero, this rate (0.0 - 1.0) will be applied to the total turnover of each shop and added to any fixed charges (to the left) to calculate the monthly bill."} + %span.icon-question-sign{'ofn-with-tip' => "When greater than zero, this rate (0.0 - 1.0) will be applied to the total turnover of each shop and added to any fixed charges (to the left) to calculate the monthly bill."} .two.columns.omega = f.number_field :account_invoices_monthly_rate, min: 0.0, max: 1.0, step: 0.01, class: "fullwidth", 'watch-value-as' => 'rate' .row .three.columns.alpha = f.label :account_invoices_monthly_cap, t(:monthly_cap_excl_tax) - %span.with-tip.icon-question-sign{'data-powertip' => "When greater than zero, this value will be used as a cap on the amount that shops will be charged each month."} + %span.icon-question-sign{'ofn-with-tip' => "When greater than zero, this value will be used as a cap on the amount that shops will be charged each month."} .two.columns.omega .input-symbol.before %span= Spree::Money.currency_symbol @@ -39,7 +42,7 @@ .row .three.columns.alpha = f.label :account_invoices_tax_rate, t(:tax_rate) - %span.with-tip.icon-question-sign{'data-powertip' => "Tax rate that applies to the the monthly bill that enterprises are charged for using the system."} + %span.icon-question-sign{'ofn-with-tip' => "Tax rate that applies to the the monthly bill that enterprises are charged for using the system."} .two.columns.omega = f.number_field :account_invoices_tax_rate, min: 0.0, max: 1.0, step: 0.01, class: "fullwidth", 'watch-value-as' => 'taxRate' @@ -59,7 +62,7 @@ .row .three.columns.alpha = label_tag :turnover, t(:example_monthly_turnover) - %span.with-tip.icon-question-sign{'data-powertip' => "An example monthly turnover for an enterprise which will be used to generate calculate an example monthly bill below."} + %span.icon-question-sign{'ofn-with-tip' => "An example monthly turnover for an enterprise which will be used to generate calculate an example monthly bill below."} .two.columns.omega .input-symbol.before %span= Spree::Money.currency_symbol @@ -67,18 +70,18 @@ .row .three.columns.alpha = label_tag :cap_reached, t(:cap_reached?) - %span.with-tip.icon-question-sign{'data-powertip' => "Whether the cap (specified to the left) has been reached, given the settings and the turnover provided."} + %span.icon-question-sign{'ofn-with-tip' => "Whether the cap (specified to the left) has been reached, given the settings and the turnover provided."} .two.columns.omega %input.fullwidth{ id: 'cap_reached', type: "text", readonly: true, ng: { value: 'capReached()' } } .row .three.columns.alpha = label_tag :included_tax, t(:included_tax) - %span.with-tip.icon-question-sign{'data-powertip' => "The total tax included in the example monthly bill, given the settings and the turnover provided."} + %span.icon-question-sign{'ofn-with-tip' => "The total tax included in the example monthly bill, given the settings and the turnover provided."} .two.columns.omega %input.fullwidth{ id: 'included_tax', type: "text", readonly: true, ng: { value: 'includedTax() | currency' } } .row .three.columns.alpha = label_tag :total_incl_tax, t(:total_monthly_bill_incl_tax) - %span.with-tip.icon-question-sign{'data-powertip' => "The example total monthly bill with tax included, given the settings and the turnover provided."} + %span.icon-question-sign{'ofn-with-tip' => "The example total monthly bill with tax included, given the settings and the turnover provided."} .two.columns.omega %input.fullwidth{ id: 'total_incl_tax', type: "text", readonly: true, ng: { value: 'total() | currency' } } diff --git a/app/views/admin/enterprise_fees/_data.html.haml b/app/views/admin/enterprise_fees/_data.html.haml new file mode 100644 index 0000000000..0b12530bff --- /dev/null +++ b/app/views/admin/enterprise_fees/_data.html.haml @@ -0,0 +1,3 @@ += admin_inject_json_ams_array "admin.enterpriseFees", "enterprises", @enterprises, Api::Admin::IdNameSerializer += admin_inject_tax_categories(module: "admin.enterpriseFees") += admin_inject_json_ams_array "admin.enterpriseFees", "calculators", @calculators, Api::Admin::CalculatorSerializer diff --git a/app/views/admin/enterprise_fees/index.html.haml b/app/views/admin/enterprise_fees/index.html.haml index 08199d4c4a..8a1e972a33 100644 --- a/app/views/admin/enterprise_fees/index.html.haml +++ b/app/views/admin/enterprise_fees/index.html.haml @@ -1,8 +1,9 @@ = content_for :page_title do Enterprise Fees -= ng_form_for @enterprise_fee_set, :url => main_app.bulk_update_admin_enterprise_fees_path, :html => {'ng-app' => 'enterprise_fees', 'ng-controller' => 'AdminEnterpriseFeesCtrl'} do |enterprise_fee_set_form| += ng_form_for @enterprise_fee_set, :url => main_app.bulk_update_admin_enterprise_fees_path, :html => {'ng-app' => 'admin.enterpriseFees', 'ng-controller' => 'enterpriseFeesCtrl'} do |enterprise_fee_set_form| = hidden_field_tag 'enterprise_id', @enterprise.id if @enterprise + = render "admin/enterprise_fees/data" = render :partial => 'spree/shared/error_messages', :locals => { :target => @enterprise_fee_set } %input.search{'ng-model' => 'query', 'placeholder' => 'Search'} @@ -19,14 +20,20 @@ %th.actions %tbody = enterprise_fee_set_form.ng_fields_for :collection do |f| - %tr{'ng-repeat' => 'enterprise_fee in enterprise_fees | filter:query'} + %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 + %ofn-select{ :id => angular_id(:enterprise_id), data: 'enterprises', include_blank: true, ng: { model: 'enterprise_fee.enterprise_id' } } + %input{ type: "hidden", name: angular_name(:enterprise_id), ng: { value: "enterprise_fee.enterprise_id" } } %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 + %ofn-select{ :id => angular_id(:tax_category_id), data: 'tax_categories', include_blank: true, ng: { model: 'enterprise_fee.tax_category_id' } } + %input{ type: "hidden", name: angular_name(:tax_category_id), 'watch-tax-category' => true } + %input{ type: "hidden", name: angular_name(:inherits_tax_category), ng: { value: "enterprise_fee.inherits_tax_category" } } + %td + %ofn-select.calculator_type{ :id => angular_id(:calculator_type), ng: { model: 'enterprise_fee.calculator_type' }, value_attr: 'name', text_attr: 'description', data: 'calculators', 'spree-ensure-calculator-preferences-match-type' => true } + %input{ type: "hidden", name: angular_name(:calculator_type), ng: { value: "enterprise_fee.calculator_type" } } %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 deleted file mode 100644 index 8dc24b5d74..0000000000 --- a/app/views/admin/enterprise_fees/index.rep +++ /dev/null @@ -1,11 +0,0 @@ -r.list_of :enterprise_fees, @presented_collection do - r.element :id - r.element :enterprise_id - 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 -end diff --git a/app/views/admin/enterprise_groups/_form_images.html.haml b/app/views/admin/enterprise_groups/_form_images.html.haml index 49169851c3..1bfffa86d2 100644 --- a/app/views/admin/enterprise_groups/_form_images.html.haml +++ b/app/views/admin/enterprise_groups/_form_images.html.haml @@ -2,16 +2,16 @@ %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'} + = f.label :logo, 'ofn-with-tip' => 'This is the logo for the group' + %div{'ofn-with-tip' => 'This is the logo for the group'} %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'} + = f.label :promo_image, 'ofn-with-tip' => 'This image is displayed at the top of the Group profile' + %div{'ofn-with-tip' => '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? diff --git a/app/views/admin/enterprise_groups/_form_users.html.haml b/app/views/admin/enterprise_groups/_form_users.html.haml index 0a8a5dd635..c4f57da48a 100644 --- a/app/views/admin/enterprise_groups/_form_users.html.haml +++ b/app/views/admin/enterprise_groups/_form_users.html.haml @@ -3,7 +3,7 @@ .row .three.columns.alpha =f.label :owner_id, 'Owner' - .with-tip{'data-powertip' => "The primary user responsible for this group."} + %div{'ofn-with-tip' => "The primary user responsible for this group."} %a What's this? .eight.columns.omega - if spree_current_user.admin? diff --git a/app/views/admin/enterprise_relationships/_form.html.haml b/app/views/admin/enterprise_relationships/_form.html.haml index 433f9f4ca0..b130faeb1a 100644 --- a/app/views/admin/enterprise_relationships/_form.html.haml +++ b/app/views/admin/enterprise_relationships/_form.html.haml @@ -1,11 +1,11 @@ %tr %td - %select{name: "enterprise_relationship_parent_id", "ng-model" => "parent_id", "ng-options" => "e.id as e.name for e in Enterprises.my_enterprises"} + %select.select2.fullwidth{id: "enterprise_relationship_parent_id", "ng-model" => "parent_id", "ng-options" => "e.id as e.name for e in Enterprises.my_enterprises"} %td permits %td - %select{name: "enterprise_relationship_child_id", "ng-model" => "child_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"} + %select.select2.fullwidth{id: "enterprise_relationship_child_id", "ng-model" => "child_id", "ng-options" => "e.id as e.name for e in Enterprises.all_enterprises"} %td %label %input{type: "checkbox", ng: {checked: "allPermissionsChecked()", click: "checkAllPermissions()"}} diff --git a/app/views/admin/enterprise_relationships/index.html.haml b/app/views/admin/enterprise_relationships/index.html.haml index 40fbdbc415..163200a0fc 100644 --- a/app/views/admin/enterprise_relationships/index.html.haml +++ b/app/views/admin/enterprise_relationships/index.html.haml @@ -9,6 +9,12 @@ = render 'search_input' %table#enterprise-relationships + %colgroup + %col{ style: "width: 30%" } + %col{ style: "width: 5%" } + %col{ style: "width: 30%" } + %col{ style: "width: 30%" } + %col{ style: "width: 5%" } %tbody = render 'form' = render 'enterprise_relationship' diff --git a/app/views/admin/enterprises/_actions.html.haml b/app/views/admin/enterprises/_actions.html.haml index ec29607747..5bcfc7a512 100644 --- a/app/views/admin/enterprises/_actions.html.haml +++ b/app/views/admin/enterprises/_actions.html.haml @@ -15,18 +15,18 @@ = link_to_with_icon 'icon-chevron-right', 'Payment Methods', spree.admin_payment_methods_path(enterprise_id: enterprise.id) (#{enterprise.payment_methods.count}) - if enterprise.payment_methods.count == 0 - %span.icon-exclamation-sign.with-tip{"data-powertip" => "This enterprise has no payment methods", style: "font-size: 16px;color: #DA5354"} + %span.icon-exclamation-sign{"ofn-with-tip" => "This enterprise has no payment methods", style: "font-size: 16px;color: #DA5354"} %br/ - if can?(:admin, Spree::ShippingMethod) && can?(:manage_shipping_methods, enterprise) = link_to_with_icon 'icon-plane', 'Shipping Methods', spree.admin_shipping_methods_path(enterprise_id: enterprise.id) (#{enterprise.shipping_methods.count}) - if enterprise.shipping_methods.count == 0 - %span.icon-exclamation-sign.with-tip{"data-powertip" => "This enterprise has shipping methods", style: "font-size: 16px;color: #DA5354"} + %span.icon-exclamation-sign{"ofn-with-tip" => "This enterprise has shipping methods", style: "font-size: 16px;color: #DA5354"} %br/ - if can?(:admin, EnterpriseFee) && can?(:manage_enterprise_fees, enterprise) = link_to_with_icon 'icon-money', 'Enterprise Fees', main_app.admin_enterprise_fees_path(enterprise_id: enterprise.id) (#{enterprise.enterprise_fees.count}) - if enterprise.enterprise_fees.count == 0 - %span.icon-warning-sign.with-tip{"data-powertip" => "This enterprise has no fees", style: "font-size: 16px;color: orange"} + %span.icon-warning-sign{"ofn-with-tip" => "This enterprise has no fees", style: "font-size: 16px;color: orange"} diff --git a/app/views/admin/enterprises/_admin_index.html.haml b/app/views/admin/enterprises/_admin_index.html.haml index 68c74a783c..2359f45f78 100644 --- a/app/views/admin/enterprises/_admin_index.html.haml +++ b/app/views/admin/enterprises/_admin_index.html.haml @@ -2,7 +2,7 @@ - if flash[:action] %p= flash[:action] -= form_for @enterprise_set, url: main_app.bulk_update_admin_enterprises_path do |f| += form_for @enterprise_set, url: main_app.bulk_update_admin_enterprises_path, html: {"ng-app" => "admin.enterprises"} do |f| %table#listing_enterprises.index %colgroup %col{style: "width: 25%;"}/ diff --git a/app/views/admin/enterprises/_form.html.haml b/app/views/admin/enterprises/_form.html.haml index 28f76d34d0..79faeea229 100644 --- a/app/views/admin/enterprises/_form.html.haml +++ b/app/views/admin/enterprises/_form.html.haml @@ -47,6 +47,10 @@ %legend Enterprise Fees = render 'admin/enterprises/form/enterprise_fees', f: f +%fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Inventory Settings'" } } + %legend Inventory Settings + = render 'admin/enterprises/form/inventory_settings', f: f + %fieldset.alpha.no-border-bottom{ ng: { show: "menu.selected.name=='Shop Preferences'" } } %legend Shop Preferences = render 'admin/enterprises/form/shop_preferences', f: f diff --git a/app/views/admin/enterprises/_new_form.html.haml b/app/views/admin/enterprises/_new_form.html.haml index a76c8ec31c..3ab9392712 100644 --- a/app/views/admin/enterprises/_new_form.html.haml +++ b/app/views/admin/enterprises/_new_form.html.haml @@ -5,10 +5,10 @@ = f.text_field :name, { placeholder: "eg. Professor Plum's Biodynamic Truffles", class: "fullwidth" } - if spree_current_user.admin? - .row{ ng: { app: "admin.users" } } + .row .three.columns.alpha =f.label :owner_id, 'Owner' - .with-tip{'data-powertip' => "The primary user responsible for this enterprise."} + %div{'ofn-with-tip' => "The primary user responsible for this enterprise."} %a What's this? .nine.columns.omega - owner_email = @enterprise.andand.owner.andand.email || "" @@ -16,7 +16,7 @@ .row .three.columns.alpha %label Primary Producer? - .with-tip{'data-powertip' => "Select 'Producer' if you are a primary producer of food."} + %div{'ofn-with-tip' => "Select 'Producer' if you are a primary producer of food."} %a What's this? .five.columns.omega = f.check_box :is_primary_producer, 'ng-model' => 'Enterprise.is_primary_producer' @@ -27,7 +27,7 @@ .alpha.eleven.columns .three.columns.alpha = f.label :sells, 'Sells' - .with-tip{'data-powertip' => "None - enterprise does not sell to customers directly.
Own - Enterprise sells own products to customers.
Any - Enterprise can sell own or other enterprises products.
"} + %div{'ofn-with-tip' => "None - enterprise does not sell to customers directly.
Own - Enterprise sells own products to customers.
Any - Enterprise can sell own or other enterprises products.
"} %a What's this? .two.columns = f.radio_button :sells, "none", 'ng-model' => 'Enterprise.sells' diff --git a/app/views/admin/enterprises/form/_images.html.haml b/app/views/admin/enterprises/form/_images.html.haml index ac960ccc1e..c03be2caa3 100644 --- a/app/views/admin/enterprises/form/_images.html.haml +++ b/app/views/admin/enterprises/form/_images.html.haml @@ -8,7 +8,7 @@ = f.file_field :logo .row .alpha.three.columns - = f.label :promo_image, class: 'with-tip', 'data-powertip' => 'This image is displayed in "About Us"' + = f.label :promo_image, 'ofn-with-tip' => 'This image is displayed in "About Us"' %br/ %span{ style: 'font-weight:bold' } PLEASE NOTE: Any promo image uploaded here will be cropped to 1200 x 260. @@ -16,4 +16,4 @@ .omega.eight.columns = image_tag @object.promo_image(:large) if @object.promo_image.present? - = f.file_field :promo_image \ No newline at end of file + = f.file_field :promo_image diff --git a/app/views/admin/enterprises/form/_inventory_settings.html.haml b/app/views/admin/enterprises/form/_inventory_settings.html.haml new file mode 100644 index 0000000000..0df0fdb35f --- /dev/null +++ b/app/views/admin/enterprises/form/_inventory_settings.html.haml @@ -0,0 +1,18 @@ +-# This is hopefully a temporary measure, pending the arrival of multiple named inventories +-# for shops. We need this here to allow hubs to restrict visible variants to only those in +-# their inventory if they so choose +.row + .alpha.eleven.columns + You may opt to manage stock levels and prices in via your + = succeed "." do + %strong + %a{href: main_app.admin_inventory_path } inventory + If you are using the inventory tool, you can select whether new products added by your suppliers need to be added to your inventory before they can be stocked. If you are not using your inventory to manage your products you should select the 'recommended' option below: +.row + .alpha.eleven.columns + .five.columns.alpha + = radio_button :enterprise, :preferred_product_selection_from_inventory_only, "0", { 'ng-model' => 'Enterprise.preferred_product_selection_from_inventory_only' } + = label :enterprise, :preferred_product_selection_from_inventory_only, "New products can be put into my shopfront (recommended)" + .six.columns.omega + = radio_button :enterprise, :preferred_product_selection_from_inventory_only, "1", { 'ng-model' => 'Enterprise.preferred_product_selection_from_inventory_only' } + = label :enterprise, :preferred_product_selection_from_inventory_only, "New products must be added to my inventory before they can be put into my shopfront" diff --git a/app/views/admin/enterprises/form/_primary_details.html.haml b/app/views/admin/enterprises/form/_primary_details.html.haml index 96f4674912..437a61e866 100644 --- a/app/views/admin/enterprises/form/_primary_details.html.haml +++ b/app/views/admin/enterprises/form/_primary_details.html.haml @@ -10,7 +10,7 @@ .alpha.eleven.columns .three.columns.alpha = f.label :group_ids, 'Groups' - .with-tip{'data-powertip' => "Select any groups or regions that you are a member of. This will help customers find your enterprise."} + %div{'ofn-with-tip' => "Select any groups or regions that you are a member of. This will help customers find your enterprise."} %a What's this? .eight.columns.omega = f.collection_select :group_ids, @groups, :id, :name, {}, class: "select2 fullwidth", multiple: true, placeholder: "Start typing to search available groups..." @@ -18,7 +18,7 @@ .row .three.columns.alpha %label Primary Producer - .with-tip{'data-powertip' => "Select 'Producer' if you are a primary producer of food."} + %div{'ofn-with-tip' => "Select 'Producer' if you are a primary producer of food."} %a What's this? .five.columns.omega = f.check_box :is_primary_producer, 'ng-model' => 'Enterprise.is_primary_producer' @@ -29,7 +29,7 @@ .alpha.eleven.columns .three.columns.alpha = f.label :sells, 'Sells' - .with-tip{'data-powertip' => "None - enterprise does not sell to customers directly.
Own - Enterprise sells own products to customers.
Any - Enterprise can sell own or other enterprises products.
"} + %div{'ofn-with-tip' => "None - enterprise does not sell to customers directly.
Own - Enterprise sells own products to customers.
Any - Enterprise can sell own or other enterprises products.
"} %a What's this? .two.columns = f.radio_button :sells, "none", 'ng-model' => 'Enterprise.sells' @@ -46,7 +46,7 @@ .row .three.columns.alpha %label Visible in search? - .with-tip{'data-powertip' => "Determines whether this enterprise will be visible to customers when searching the site."} + %div{'ofn-with-tip' => "Determines whether this enterprise will be visible to customers when searching the site."} %a What's this? .two.columns = f.radio_button :visible, true @@ -60,7 +60,7 @@ .row{ ng: { show: "Enterprise.sells == 'own' || Enterprise.sells == 'any'" } } .three.columns.alpha = f.label :permalink, 'Permalink (no spaces)' - .with-tip{'data-powertip' => "This permalink is used to create the url to your shop: #{spree.root_url}your-shop-name/shop"} + %div{'ofn-with-tip' => "This permalink is used to create the url to your shop: #{spree.root_url}your-shop-name/shop"} %a What's this? .six.columns = f.text_field :permalink, { 'ng-model' => "Enterprise.permalink", placeholder: "eg. your-shop-name", 'ng-model-options' => "{ updateOn: 'default blur', debounce: {'default': 300, 'blur': 0} }" } @@ -72,7 +72,7 @@ .row{ ng: { show: "Enterprise.sells == 'own' || Enterprise.sells == 'any'" } } .three.columns.alpha %label Link to shop front - .with-tip{'data-powertip' => "A direct link to your shopfront on the Open Food Network."} + %div{'ofn-with-tip' => "A direct link to your shopfront on the Open Food Network."} %a What's this? .eight.columns.omega = surround spree.root_url, "/shop" do diff --git a/app/views/admin/enterprises/form/_users.html.haml b/app/views/admin/enterprises/form/_users.html.haml index d13723795d..289a85d7a1 100644 --- a/app/views/admin/enterprises/form/_users.html.haml +++ b/app/views/admin/enterprises/form/_users.html.haml @@ -6,7 +6,7 @@ =f.label :owner_id, 'Owner' - if full_permissions %span.required * - .with-tip{'data-powertip' => "The primary user responsible for this enterprise."} + %div{'ofn-with-tip' => "The primary user responsible for this enterprise."} %a What's this? .eight.columns.omega - if full_permissions @@ -19,7 +19,7 @@ =f.label :user_ids, 'Managers' - if full_permissions %span.required * - .with-tip{'data-powertip' => "The other users with permission to manage this enterprise."} + %div{'ofn-with-tip' => "The other users with permission to manage this enterprise."} %a What's this? .eight.columns.omega - if full_permissions diff --git a/app/views/admin/enterprises/new.html.haml b/app/views/admin/enterprises/new.html.haml index 8f21067941..4413acfc2c 100644 --- a/app/views/admin/enterprises/new.html.haml +++ b/app/views/admin/enterprises/new.html.haml @@ -11,5 +11,5 @@ = form_for [main_app, :admin, @enterprise], html: { "nav-check" => '', "nav-callback" => '' } do |f| .row - .twelve.columns.fullwidth_inputs + .twelve.columns.fullwidth_inputs{ ng: { app: "admin.users" } } = render 'new_form', f: f diff --git a/app/views/admin/order_cycles/_advanced_settings.html.haml b/app/views/admin/order_cycles/_advanced_settings.html.haml new file mode 100644 index 0000000000..27b347880b --- /dev/null +++ b/app/views/admin/order_cycles/_advanced_settings.html.haml @@ -0,0 +1,22 @@ +.row + .alpha.omega.sixteen.columns + %h3 Advanced Settings + += form_for [main_app, :admin, @order_cycle] do |f| + .row + .six.columns.alpha + = f.label "enterprise_preferred_product_selection_from_coordinator_inventory_only", t('admin.order_cycle.choose_products_from') + .with-tip{'data-powertip' => "You can opt to restrict all available products (both incoming and outgoing), to only those in #{@order_cycle.coordinator.name}'s inventory."} + %a What's this? + .four.columns + = f.radio_button :preferred_product_selection_from_coordinator_inventory_only, true + = f.label :preferred_product_selection_from_coordinator_inventory_only, "Coordinator's Inventory Only" + .six.columns.omega + = f.radio_button :preferred_product_selection_from_coordinator_inventory_only, false + = f.label :preferred_product_selection_from_coordinator_inventory_only, "All Available Products" + + .row + .sixteen.columns.alpha.omega.text-center + %input{ type: 'submit', value: 'Save and Reload Page' } + or + %a{ href: "#", onClick: "toggleSettings()" } Close 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 78e64fc2d0..e2218599ce 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 @@ -9,14 +9,14 @@ .exchange-product{'ng-repeat' => 'product in supplied_products | filter:productSuppliedToOrderCycle | visibleProducts:exchange:order_cycle.visible_variants_for_outgoing_exchanges | orderBy:"name"' } .exchange-product-details %label - = 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' + -# MASTER_VARIANTS: No longer required + -# = 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 }}'} .name {{ product.name }} .supplier {{ product.supplier_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'} + .exchange-product-variant{'ng-repeat' => 'variant in product.variants | visibleVariants:exchange:order_cycle.visible_variants_for_outgoing_exchanges | 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 }}', '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' diff --git a/app/views/admin/order_cycles/_row.html.haml b/app/views/admin/order_cycles/_row.html.haml index 2594d5c9f3..d75a97bbb5 100644 --- a/app/views/admin/order_cycles/_row.html.haml +++ b/app/views/admin/order_cycles/_row.html.haml @@ -11,7 +11,7 @@ - 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} + %span{'ofn-with-tip' => supplier_list} = suppliers.count suppliers - else @@ -21,7 +21,7 @@ - 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} + %span{'ofn-with-tip' => distributor_list} = distributors.count distributors - else diff --git a/app/views/admin/order_cycles/edit.html.haml b/app/views/admin/order_cycles/edit.html.haml index 12dc6238ae..d2b7605289 100644 --- a/app/views/admin/order_cycles/edit.html.haml +++ b/app/views/admin/order_cycles/edit.html.haml @@ -1,9 +1,27 @@ -- if can? :notify_producers, @order_cycle - = content_for :page_actions do +- content_for :page_actions do + :javascript + function toggleSettings(){ + if( $('#advanced_settings').is(":visible") ){ + $('button#toggle_settings i').switchClass("icon-chevron-up","icon-chevron-down") + } + else { + $('button#toggle_settings i').switchClass("icon-chevron-down","icon-chevron-up") + } + $("#advanced_settings").slideToggle() + } + + - if can? :notify_producers, @order_cycle %li = button_to "Notify producers", main_app.notify_producers_admin_order_cycle_path, :id => 'admin_notify_producers', :confirm => 'Are you sure?' + %li + %button#toggle_settings{ onClick: 'toggleSettings()' } + Advanced Settings + %i.icon-chevron-down +#advanced_settings{ hidden: true } + = render partial: "/admin/order_cycles/advanced_settings" + %h1 Edit Order Cycle diff --git a/app/views/admin/order_cycles/index.html.haml b/app/views/admin/order_cycles/index.html.haml index e39fdde12a..8f062ff7db 100644 --- a/app/views/admin/order_cycles/index.html.haml +++ b/app/views/admin/order_cycles/index.html.haml @@ -11,7 +11,7 @@ %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| += form_for @order_cycle_set, url: main_app.bulk_update_admin_order_cycles_path, html: {"ng-app" => "admin.orderCycles"} do |f| %table.index#listing_order_cycles %colgroup %col diff --git a/app/views/admin/shared/_bulk_actions_dropdown.html.haml b/app/views/admin/shared/_bulk_actions_dropdown.html.haml index 912fe6662a..0c8ac170a1 100644 --- a/app/views/admin/shared/_bulk_actions_dropdown.html.haml +++ b/app/views/admin/shared/_bulk_actions_dropdown.html.haml @@ -1,7 +1,6 @@ -.three.columns - .ofn-drop-down#bulk-actions-dropdown{ 'ng-controller' => "DropDownCtrl" } - %span.icon-check   Actions - %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } - %div.menu{ 'ng-show' => "expanded" } - .three.columns.alpha.menu_item{ 'ng-repeat' => "action in bulkActions", 'ng-click' => "$eval(action.callback)(filteredLineItems)", 'ofn-close-on-click' => true } - %span.three.columns.omega {{action.name }} +.ofn-drop-down#bulk-actions-dropdown + %span.icon-check= "  #{t('admin.actions')}".html_safe + %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } + .menu{ 'ng-show' => "expanded" } + .menu_item{ 'ng-repeat' => "action in bulkActions", 'ng-click' => "$eval(action.callback)(filteredLineItems)", 'close-on-click' => true } + %span.name {{ action.name }} diff --git a/app/views/admin/shared/_columns_dropdown.html.haml b/app/views/admin/shared/_columns_dropdown.html.haml index b16d388c1d..4663443321 100644 --- a/app/views/admin/shared/_columns_dropdown.html.haml +++ b/app/views/admin/shared/_columns_dropdown.html.haml @@ -1,8 +1,7 @@ -%div.three.columns.omega - %div.ofn-drop-down.right#columns-dropdown{ 'ng-controller' => "DropDownCtrl" } - %span{ :class => 'icon-reorder' }   Columns - %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } - %div.menu{ 'ng-show' => "expanded" } - %div.menu_item.three.columns.alpha.omega{ 'ng-repeat' => "column in columns", 'ofn-toggle-column' => true } - %span.one.column.alpha.text-center {{ column.visible && "✓" || !column.visible && " " }} - %span.two.columns.omega {{column.name }} +.ofn-drop-down.right#columns-dropdown + %span{ :class => 'icon-reorder' }= "  #{t('admin.columns')}".html_safe + %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } + %div.menu{ 'ng-show' => "expanded" } + %div.menu_item{ ng: { repeat: "column in columns" }, toggle: { column: true } } + %span.check + %span.name {{column.name }} diff --git a/app/views/admin/shared/_views_dropdown.html.haml b/app/views/admin/shared/_views_dropdown.html.haml new file mode 100644 index 0000000000..9fe3df8521 --- /dev/null +++ b/app/views/admin/shared/_views_dropdown.html.haml @@ -0,0 +1,7 @@ +.ofn-drop-down#views-dropdown + %span{ :class => 'icon-eye-open' }= "  #{t('admin.viewing', current_view_name: '{{ currentView().name }}')}".html_safe + %span{ 'ng-class' => "expanded && 'icon-caret-up' || !expanded && 'icon-caret-down'" } + %div.menu{ 'ng-show' => "expanded" } + %div.menu_item{ ng: { repeat: "(viewKey, view) in views" }, toggle: { view: true }, 'close-on-click' => true } + %span.check + %span.name {{ view.name }} diff --git a/app/views/admin/variant_overrides/_controls.html.haml b/app/views/admin/variant_overrides/_controls.html.haml new file mode 100644 index 0000000000..4a25677be3 --- /dev/null +++ b/app/views/admin/variant_overrides/_controls.html.haml @@ -0,0 +1,15 @@ +%hr.divider.sixteen.columns.alpha.omega{ ng: { show: 'hub_id && products.length > 0' } } +.controls.sixteen.columns.alpha.omega{ ng: { show: 'hub_id && products.length > 0' } } + .eight.columns.alpha + = render 'admin/shared/bulk_actions_dropdown' + = render 'admin/shared/views_dropdown' + %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.inventory.visible' } , data: { powertip: "#{t('admin.inventory.inventory_powertip')}" } } + %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.hidden.visible' } , data: { powertip: "#{t('admin.inventory.hidden_powertip')}" } } + %span.text-big.with-tip.icon-question-sign{ ng: { show: 'views.new.visible' } , data: { powertip: "#{t('admin.inventory.new_powertip')}" } } + .four.columns   + .four.columns.omega{ ng: { show: 'views.new.visible' } } + %button.fullwidth{ type: 'button', ng: { click: "selectView('inventory')" } } + %i.icon-chevron-left + Back to my inventory + .four.columns.omega{ng: { show: 'views.inventory.visible' } } + = render 'admin/shared/columns_dropdown' diff --git a/app/views/admin/variant_overrides/_data.html.haml b/app/views/admin/variant_overrides/_data.html.haml index 64a7619ea7..9d371415fe 100644 --- a/app/views/admin/variant_overrides/_data.html.haml +++ b/app/views/admin/variant_overrides/_data.html.haml @@ -3,3 +3,4 @@ = admin_inject_hub_permissions = admin_inject_producers module: 'admin.variantOverrides' = admin_inject_variant_overrides += admin_inject_inventory_items module: 'admin.variantOverrides' diff --git a/app/views/admin/variant_overrides/_filters.html.haml b/app/views/admin/variant_overrides/_filters.html.haml index 9dc90d7ad3..7330c26799 100644 --- a/app/views/admin/variant_overrides/_filters.html.haml +++ b/app/views/admin/variant_overrides/_filters.html.haml @@ -1,17 +1,17 @@ .filters.sixteen.columns.alpha .filter.four.columns.alpha - %label{ :for => 'query', ng: {class: '{disabled: !hub.id}'} }Quick Search + %label{ :for => 'query', ng: {class: '{disabled: !hub_id}'} }=t('admin.quick_search') %br - %input.fullwidth{ :type => "text", :id => 'query', ng: { model: 'query', disabled: '!hub.id'} } + %input.fullwidth{ :type => "text", :id => 'query', ng: { model: 'query', disabled: '!hub_id'} } .two.columns   .filter_select.four.columns - %label{ :for => 'hub_id', ng: { bind: 'hub_id ? "Shop" : "Select a shop"' } } + %label{ :for => 'hub_id', ng: { bind: "hub_id ? '#{t('admin.shop')}' : '#{t('admin.inventory.select_a_shop')}'" } } %br - %select.select2.fullwidth#hub_id{ 'ng-model' => 'hub_id', name: 'hub_id', ng: { options: 'hub.id as hub.name for (id, hub) in hubs', change: 'selectHub()' } } + %select.select2.fullwidth#hub_id{ 'ng-model' => 'hub_id', name: 'hub_id', ng: { options: 'hub.id as hub.name for (id, hub) in hubs' } } .filter_select.four.columns - %label{ :for => 'producer_filter', ng: {class: '{disabled: !hub.id}'} }Producer + %label{ :for => 'producer_filter', ng: {class: '{disabled: !hub_id}'} }=t('admin.producer') %br - %input.ofn-select2.fullwidth{ :id => 'producer_filter', type: 'number', style: 'display:none', data: 'producers', blank: "{id: 0, name: 'All'}", ng: { model: 'producerFilter', disabled: '!hub.id' } } + %input.ofn-select2.fullwidth{ :id => 'producer_filter', type: 'number', data: 'producers', blank: "{id: 0, name: 'All'}", ng: { model: 'producerFilter', disabled: '!hub_id' } } -# .filter_select{ :class => "three columns" } -# %label{ :for => 'distributor_filter' }Hub -# %br @@ -23,4 +23,4 @@ .filter_clear.two.columns.omega %label{ :for => 'clear_all_filters' } %br - %input.red.fullwidth{ :type => 'button', :id => 'clear_all_filters', :value => "Clear All", ng: { click: "resetSelectFilters()", disabled: '!hub.id'} } + %input.red.fullwidth{ :type => 'button', :id => 'clear_all_filters', :value => "#{t('admin.clear_all')}", ng: { click: "resetSelectFilters()", disabled: '!hub_id'} } diff --git a/app/views/admin/variant_overrides/_header.html.haml b/app/views/admin/variant_overrides/_header.html.haml index 7b4a38db47..e4a8e98420 100644 --- a/app/views/admin/variant_overrides/_header.html.haml +++ b/app/views/admin/variant_overrides/_header.html.haml @@ -1,4 +1,8 @@ +- content_for :html_title do + = t("admin.inventory.title") + - content_for :page_title do - Override Product Details + %h1.page-title= t("admin.inventory.title") + %a.with-tip{ 'data-powertip' => "#{t("admin.inventory.description")}" }=t('admin.whats_this') = render :partial => 'spree/admin/shared/product_sub_menu' diff --git a/app/views/admin/variant_overrides/_hidden_products.html.haml b/app/views/admin/variant_overrides/_hidden_products.html.haml new file mode 100644 index 0000000000..902e24a232 --- /dev/null +++ b/app/views/admin/variant_overrides/_hidden_products.html.haml @@ -0,0 +1,22 @@ +%div{ ng: { show: 'views.hidden.visible' } } + %table#hidden-products{ ng: { show: 'filteredProducts.length > 0' } } + %col.producer{ width: "20%" } + %col.product{ width: "20%" } + %col.variant{ width: "30%" } + %col.add{ width: "15%" } + %thead + %tr + %th.producer=t('admin.producer') + %th.product=t('admin.product') + %th.variant=t('(admin.variant') + %th.add=t('admin.inventory.add') + %tbody{ bindonce: true, ng: { repeat: 'product in filteredProducts | limitTo:productLimit' } } + %tr{ id: "v_{{variant.id}}", ng: { repeat: 'variant in product.variants | inventoryVariants:hub_id:views' } } + %td.producer{ bo: { bind: 'producersByID[product.producer_id].name'} } + %td.product{ bo: { bind: 'product.name'} } + %td.variant + %span{ bo: { bind: 'variant.display_name || ""'} } + .variant-override-unit{ bo: { bind: 'variant.unit_to_display'} } + %td.add + %button.fullwidth.icon-plus{ ng: { click: "setVisibility(hub_id,variant.id,true)" } } + = t('admin.inventory.add') diff --git a/app/views/admin/variant_overrides/_loading_flash.html.haml b/app/views/admin/variant_overrides/_loading_flash.html.haml new file mode 100644 index 0000000000..c543a5517c --- /dev/null +++ b/app/views/admin/variant_overrides/_loading_flash.html.haml @@ -0,0 +1,3 @@ +%div.sixteen.columns.alpha.omega#loading{ ng: { cloak: true, if: 'hub_id && products.length == 0 && RequestMonitor.loading' } } + %img.spinner{ src: "/assets/spinning-circles.svg" } + %h1 LOADING INVENTORY diff --git a/app/views/admin/variant_overrides/_new_products.html.haml b/app/views/admin/variant_overrides/_new_products.html.haml new file mode 100644 index 0000000000..86ca180b8f --- /dev/null +++ b/app/views/admin/variant_overrides/_new_products.html.haml @@ -0,0 +1,26 @@ +%table#new-products{ ng: { show: 'views.new.visible && filteredProducts.length > 0' } } + %col.producer{ width: "20%" } + %col.product{ width: "20%" } + %col.variant{ width: "30%" } + %col.add{ width: "15%" } + %col.hide{ width: "15%" } + %thead + %tr + %th.producer=t('admin.producer') + %th.product=t('admin.product') + %th.variant=t('(admin.variant') + %th.add=t('admin.inventory.add') + %th.hide=t('admin.inventory.hide') + %tbody{ bindonce: true, ng: { repeat: 'product in filteredProducts | limitTo:productLimit' } } + %tr{ id: "v_{{variant.id}}", ng: { repeat: 'variant in product.variants | inventoryVariants:hub_id:views' } } + %td.producer{ bo: { bind: 'producersByID[product.producer_id].name'} } + %td.product{ bo: { bind: 'product.name'} } + %td.variant + %span{ bo: { bind: 'variant.display_name || ""'} } + .variant-override-unit{ bo: { bind: 'variant.unit_to_display'} } + %td.add + %button.fullwidth.icon-plus{ ng: { click: "setVisibility(hub_id,variant.id,true)" } } + = t('admin.inventory.add') + %td.hide + %button.fullwidth.hide.icon-remove{ ng: { click: "setVisibility(hub_id,variant.id,false)" } } + = t('admin.inventory.hide') diff --git a/app/views/admin/variant_overrides/_new_products_alert.html.haml b/app/views/admin/variant_overrides/_new_products_alert.html.haml new file mode 100644 index 0000000000..29ec4c9623 --- /dev/null +++ b/app/views/admin/variant_overrides/_new_products_alert.html.haml @@ -0,0 +1,5 @@ +%div{ ng: { show: '(newProductCount = (products | hubPermissions:hubPermissions:hub_id | newInventoryProducts:hub_id).length) > 0 && !views.new.visible && !alertDismissed' } } + %hr.divider.sixteen.columns.alpha.omega + %alert-row{ message: "#{t('admin.inventory.new_products_alert_message', new_product_count: '{{ newProductCount }}')}", + dismissed: "alertDismissed", + button: { text: "#{t('admin.inventory.review_now')}", action: "selectView('new')" } } diff --git a/app/views/admin/variant_overrides/_no_results.html.haml b/app/views/admin/variant_overrides/_no_results.html.haml new file mode 100644 index 0000000000..cdec6ab8c5 --- /dev/null +++ b/app/views/admin/variant_overrides/_no_results.html.haml @@ -0,0 +1,7 @@ +%div.text-big.no-results{ ng: { show: 'hub_id && products.length > 0 && filteredProducts.length == 0' } } + %span{ ng: { show: 'views.inventory.visible && !filtersApplied()' } }=t('admin.inventory.currently_empty') + %span{ ng: { show: 'views.inventory.visible && filtersApplied()' } }=t('admin.inventory.no_matching_products') + %span{ ng: { show: 'views.hidden.visible && !filtersApplied()' } }=t('admin.inventory.no_hidden_products') + %span{ ng: { show: 'views.hidden.visible && filtersApplied()' } }=t('admin.inventory.no_matching_hidden_products') + %span{ ng: { show: 'views.new.visible && !filtersApplied()' } }=t('admin.inventory.no_new_products') + %span{ ng: { show: 'views.new.visible && filtersApplied()' } }=t('admin.inventory.no_matching_new_products') diff --git a/app/views/admin/variant_overrides/_products.html.haml b/app/views/admin/variant_overrides/_products.html.haml index ed3354de9a..c17e4c189a 100644 --- a/app/views/admin/variant_overrides/_products.html.haml +++ b/app/views/admin/variant_overrides/_products.html.haml @@ -1,23 +1,27 @@ -%table.index.bulk{ ng: {show: 'hub'}} - %col.producer{ width: "20%", ng: { show: 'columns.producer.visible' } } - %col.product{ width: "20%", ng: { show: 'columns.product.visible' } } - %col.sku{ width: "20%", ng: { show: 'columns.sku.visible' } } - %col.price{ width: "10%", ng: { show: 'columns.price.visible' } } - %col.on_hand{ width: "10%", ng: { show: 'columns.on_hand.visible' } } - %col.on_demand{ width: "10%", ng: { show: 'columns.on_demand.visible' } } - %col.reset{ width: "1%", ng: { show: 'columns.reset.visible' } } - %col.reset{ width: "15%", ng: { show: 'columns.reset.visible' } } - %col.inheritance{ width: "5%", ng: { show: 'columns.inheritance.visible' } } - %thead - %tr{ ng: { controller: "ColumnsCtrl" } } - %th.producer{ ng: { show: 'columns.producer.visible' } } Producer - %th.product{ ng: { show: 'columns.product.visible' } } Product - %th.sku{ ng: { show: 'columns.sku.visible' } } SKU - %th.price{ ng: { show: 'columns.price.visible' } } Price - %th.on_hand{ ng: { show: 'columns.on_hand.visible' } } On hand - %th.on_demand{ ng: { show: 'columns.on_demand.visible' } } On Demand? - %th.reset{ colspan: 2, ng: { show: 'columns.reset.visible' } } Enable Stock Level Reset? - %th.inheritance{ ng: { show: 'columns.inheritance.visible' } } Inherit? - %tbody{bindonce: true, ng: {repeat: 'product in products | hubPermissions:hubPermissions:hub.id | attrFilter:{producer_id:producerFilter} | filter:query' } } - = render 'admin/variant_overrides/products_product' - = render 'admin/variant_overrides/products_variants' +%form{ name: 'variant_overrides_form', ng: { show: "views.inventory.visible" } } + %save-bar{ save: "update()", form: "variant_overrides_form" } + %table.index.bulk#variant-overrides + %col.producer{ width: "20%", ng: { show: 'columns.producer.visible' } } + %col.product{ width: "20%", ng: { show: 'columns.product.visible' } } + %col.sku{ width: "20%", ng: { show: 'columns.sku.visible' } } + %col.price{ width: "10%", ng: { show: 'columns.price.visible' } } + %col.on_hand{ width: "10%", ng: { show: 'columns.on_hand.visible' } } + %col.on_demand{ width: "10%", ng: { show: 'columns.on_demand.visible' } } + %col.reset{ width: "1%", ng: { show: 'columns.reset.visible' } } + %col.reset{ width: "15%", ng: { show: 'columns.reset.visible' } } + %col.inheritance{ width: "5%", ng: { show: 'columns.inheritance.visible' } } + %col.visibility{ width: "10%", ng: { show: 'columns.visibility.visible' } } + %thead + %tr{ ng: { controller: "ColumnsCtrl" } } + %th.producer{ ng: { show: 'columns.producer.visible' } }=t('admin.producer') + %th.product{ ng: { show: 'columns.product.visible' } }=t('admin.product') + %th.sku{ ng: { show: 'columns.sku.visible' } }=t('admin.inventory.sku') + %th.price{ ng: { show: 'columns.price.visible' } }=t('admin.inventory.price') + %th.on_hand{ ng: { show: 'columns.on_hand.visible' } }=t('admin.inventory.on_hand') + %th.on_demand{ ng: { show: 'columns.on_demand.visible' } }=t('admin.inventory.on_demand') + %th.reset{ colspan: 2, ng: { show: 'columns.reset.visible' } }=t('admin.inventory.enable_reset') + %th.inheritance{ ng: { show: 'columns.inheritance.visible' } }=t('admin.inventory.inherit') + %th.visibility{ ng: { show: 'columns.visibility.visible' } }=t('admin.inventory.hide') + %tbody{bindonce: true, ng: {repeat: 'product in filteredProducts = (products | hubPermissions:hubPermissions:hub_id | inventoryProducts:hub_id:views | attrFilter:{producer_id:producerFilter} | filter:query) | limitTo:productLimit' } } + = render 'admin/variant_overrides/products_product' + = render 'admin/variant_overrides/products_variants' diff --git a/app/views/admin/variant_overrides/_products_product.html.haml b/app/views/admin/variant_overrides/_products_product.html.haml index b7cb11041b..70b48e3909 100644 --- a/app/views/admin/variant_overrides/_products_product.html.haml +++ b/app/views/admin/variant_overrides/_products_product.html.haml @@ -7,3 +7,4 @@ %td.on_demand{ ng: { show: 'columns.on_demand.visible' } } %td.reset{ colspan: 2, ng: { show: 'columns.reset.visible' } } %td.inheritance{ ng: { show: 'columns.inheritance.visible' } } + %td.visibility{ ng: { show: 'columns.visibility.visible' } } diff --git a/app/views/admin/variant_overrides/_products_variants.html.haml b/app/views/admin/variant_overrides/_products_variants.html.haml index 87ec1709e4..c26be92697 100644 --- a/app/views/admin/variant_overrides/_products_variants.html.haml +++ b/app/views/admin/variant_overrides/_products_variants.html.haml @@ -1,19 +1,22 @@ -%tr.variant{ id: "v_{{variant.id}}", ng: {repeat: 'variant in product.variants'}} +%tr.variant{ id: "v_{{variant.id}}", ng: {repeat: 'variant in product.variants | inventoryVariants:hub_id:views'}} %td.producer{ ng: { show: 'columns.producer.visible' } } %td.product{ ng: { show: 'columns.product.visible' } } %span{ bo: { bind: 'variant.display_name || ""'} } .variant-override-unit{ bo: { bind: 'variant.unit_to_display'} } %td.sku{ ng: { show: 'columns.sku.visible' } } - %input{name: 'variant-overrides-{{ variant.id }}-sku', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].sku'}, placeholder: '{{ variant.sku }}', 'ofn-track-variant-override' => 'sku'} + %input{name: 'variant-overrides-{{ variant.id }}-sku', type: 'text', ng: {model: 'variantOverrides[hub_id][variant.id].sku'}, placeholder: '{{ variant.sku }}', 'ofn-track-variant-override' => 'sku'} %td.price{ ng: { show: 'columns.price.visible' } } - %input{name: 'variant-overrides-{{ variant.id }}-price', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].price'}, placeholder: '{{ variant.price }}', 'ofn-track-variant-override' => 'price'} + %input{name: 'variant-overrides-{{ variant.id }}-price', type: 'text', ng: {model: 'variantOverrides[hub_id][variant.id].price'}, placeholder: '{{ variant.price }}', 'ofn-track-variant-override' => 'price'} %td.on_hand{ ng: { show: 'columns.on_hand.visible' } } - %input{name: 'variant-overrides-{{ variant.id }}-count_on_hand', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].count_on_hand'}, placeholder: '{{ variant.on_hand }}', 'ofn-track-variant-override' => 'count_on_hand'} + %input{name: 'variant-overrides-{{ variant.id }}-count_on_hand', type: 'text', ng: {model: 'variantOverrides[hub_id][variant.id].count_on_hand'}, placeholder: '{{ variant.on_hand }}', 'ofn-track-variant-override' => 'count_on_hand'} %td.on_demand{ ng: { show: 'columns.on_demand.visible' } } - %input.field{ :type => 'checkbox', name: 'variant-overrides-{{ variant.id }}-on_demand', ng: { model: 'variantOverrides[hub.id][variant.id].on_demand' }, 'ofn-track-variant-override' => 'on_demand' } + %input.field{ :type => 'checkbox', name: 'variant-overrides-{{ variant.id }}-on_demand', ng: { model: 'variantOverrides[hub_id][variant.id].on_demand' }, 'ofn-track-variant-override' => 'on_demand' } %td.reset{ ng: { show: 'columns.reset.visible' } } - %input{name: 'variant-overrides-{{ variant.id }}-resettable', type: 'checkbox', ng: {model: 'variantOverrides[hub.id][variant.id].resettable'}, placeholder: '{{ variant.resettable }}', 'ofn-track-variant-override' => 'resettable'} + %input{name: 'variant-overrides-{{ variant.id }}-resettable', type: 'checkbox', ng: {model: 'variantOverrides[hub_id][variant.id].resettable'}, placeholder: '{{ variant.resettable }}', 'ofn-track-variant-override' => 'resettable'} %td.reset{ ng: { show: 'columns.reset.visible' } } - %input{name: 'variant-overrides-{{ variant.id }}-default_stock', type: 'text', ng: {model: 'variantOverrides[hub.id][variant.id].default_stock'}, placeholder: '{{ variant.default_stock ? variant.default_stock : "Default stock"}}', 'ofn-track-variant-override' => 'default_stock'} + %input{name: 'variant-overrides-{{ variant.id }}-default_stock', type: 'text', ng: {model: 'variantOverrides[hub_id][variant.id].default_stock'}, placeholder: '{{ variant.default_stock ? variant.default_stock : "Default stock"}}', 'ofn-track-variant-override' => 'default_stock'} %td.inheritance{ ng: { show: 'columns.inheritance.visible' } } %input.field{ :type => 'checkbox', name: 'variant-overrides-{{ variant.id }}-inherit', ng: { model: 'inherit' }, 'track-inheritance' => true } + %td.visibility{ ng: { show: 'columns.visibility.visible' } } + %button.icon-remove.hide.fullwidth{ :type => 'button', ng: { click: "setVisibility(hub_id,variant.id,false)" } } + = t('admin.inventory.hide') diff --git a/app/views/admin/variant_overrides/_show_more.html.haml b/app/views/admin/variant_overrides/_show_more.html.haml new file mode 100644 index 0000000000..ad943c853e --- /dev/null +++ b/app/views/admin/variant_overrides/_show_more.html.haml @@ -0,0 +1,4 @@ +.sixteen.columns.alpha.omega.text-center{ ng: {show: 'productLimit < filteredProducts.length'}} + %input{ type: 'button', value: 'Show More', ng: { click: 'productLimit = productLimit + 10' } } + or + %input{ type: 'button', value: "Show All ({{ filteredProducts.length - productLimit }} More)", ng: { click: 'productLimit = filteredProducts.length' } } diff --git a/app/views/admin/variant_overrides/index.html.haml b/app/views/admin/variant_overrides/index.html.haml index 9027445e62..16beaad7b9 100644 --- a/app/views/admin/variant_overrides/index.html.haml +++ b/app/views/admin/variant_overrides/index.html.haml @@ -1,14 +1,14 @@ = render 'admin/variant_overrides/header' = render 'admin/variant_overrides/data' -%div{ ng: { app: 'admin.variantOverrides', controller: 'AdminVariantOverridesCtrl', init: 'initialise()' } } +.margin-bottom-50{ ng: { app: 'admin.variantOverrides', controller: 'AdminVariantOverridesCtrl', init: 'initialise()' } } = render 'admin/variant_overrides/filters' - %hr.divider.sixteen.columns.alpha.omega{ ng: { show: 'hub' } } - .controls.sixteen.columns.alpha.omega{ ng: { show: 'hub' } } - %input.four.columns.alpha{ type: 'button', value: 'Reset Stock to Defaults', 'ng-click' => 'resetStock()' } - %div.nine.columns.alpha   - = render 'admin/shared/columns_dropdown' - - %form{ name: 'variant_overrides_form' } - %save-bar{ save: "update()", form: "variant_overrides_form" } + = render 'admin/variant_overrides/new_products_alert' + = render 'admin/variant_overrides/loading_flash' + = render 'admin/variant_overrides/controls' + = render 'admin/variant_overrides/no_results' + %div{ ng: { cloak: true, show: 'hub_id && filteredProducts.length > 0' } } + = render 'admin/variant_overrides/new_products' + = render 'admin/variant_overrides/hidden_products' = render 'admin/variant_overrides/products' + = render 'admin/variant_overrides/show_more' diff --git a/app/views/shared/_account_sidebar.html.haml b/app/views/shared/_account_sidebar.html.haml deleted file mode 100644 index e572979eb1..0000000000 --- a/app/views/shared/_account_sidebar.html.haml +++ /dev/null @@ -1,20 +0,0 @@ --##account{"ng-controller" => "AccountSidebarCtrl"} - -#.row - -#.panel - -#%p - -#%strong= link_to "Manage my account", account_path - -#- if enterprise_user? - -#%strong= link_to "Enterprise admin", admin_path - -#- if order = last_completed_order - -#%dl - -#%dt Current Hub: - -#%dd= link_to current_distributor.name, main_app.shop_path - -#%br - -#%dt Last hub: - -#%dd - -#- if order.distributor != current_distributor - -#= link_to "#{order.distributor.name}".html_safe, "", - -#{class: distributor_link_class(order.distributor), - -#"ng-click" => "emptyCart('#{main_app.enterprise_shop_path(order.distributor)}', $event)"} - -#- else - -#= order.distributor.name diff --git a/app/views/shared/_sidebar.html.haml b/app/views/shared/_sidebar.html.haml deleted file mode 100644 index 8fdcf7c2b5..0000000000 --- a/app/views/shared/_sidebar.html.haml +++ /dev/null @@ -1,12 +0,0 @@ --#%aside#sidebar.right-off-canvas-menu{ role: "complementary", "ng-controller" => "SidebarCtrl", --#"ng-class" => "{'active' : Sidebar.active()}"} - - -#- if spree_current_user.nil? - -#%tabset - -#= render partial: "shared/login_sidebar" - -#= render partial: "shared/signup_sidebar" - -#= render partial: "shared/forgot_sidebar" - -#- else - -#= render partial: "shared/account_sidebar" - - -#= yield :sidebar diff --git a/app/views/shared/_signed_in.html.haml b/app/views/shared/_signed_in.html.haml index c3a1bd8cc8..0af52d6d94 100644 --- a/app/views/shared/_signed_in.html.haml +++ b/app/views/shared/_signed_in.html.haml @@ -1,3 +1,9 @@ +- if discourse_configured? + %li + %a{href: main_app.discourse_login_path, target: '_blank'} + %span.nav-primary + = t 'label_notices' + %li.has-dropdown.not-click %a{href: "#"} diff --git a/app/views/shared/_signed_in_offcanvas.html.haml b/app/views/shared/_signed_in_offcanvas.html.haml index 839d1c08fe..f34a03068f 100644 --- a/app/views/shared/_signed_in_offcanvas.html.haml +++ b/app/views/shared/_signed_in_offcanvas.html.haml @@ -1,3 +1,10 @@ +- if discourse_configured? + %li.li-menu + %a{href: main_app.discourse_login_path, target: '_blank'} + %span.nav-primary + %i.ofn-i_025-notepad + = t 'label_notices' + - if admin_user? or enterprise_user? %li %a{href: spree.admin_path, target:'_blank'} diff --git a/app/views/shared/mailers/_social_and_contact.html.haml b/app/views/shared/mailers/_social_and_contact.html.haml index 4f5222f77d..d76925c641 100644 --- a/app/views/shared/mailers/_social_and_contact.html.haml +++ b/app/views/shared/mailers/_social_and_contact.html.haml @@ -24,6 +24,5 @@ %h5 = t :email_contact %strong - %a{href: ContentConfig.footer_email.reverse, mailto: true, target: '_blank'} - #{ContentConfig.footer_email} + = mail_to ContentConfig.footer_email %span.clear diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index 0ab782023d..003c1ab5f8 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -1,10 +1,13 @@ +- content_for :app_wrapper_attrs do + = "ng-app='admin.lineItems'" + - content_for :page_title do %h1.page-title Bulk Order Management - %a.with-tip{ 'data-powertip' => "Use this page to alter product quantities across multiple orders. Products may also be removed from orders entirely, if required." } What's this? + %a{ 'ofn-with-tip' => "Use this page to alter product quantities across multiple orders. Products may also be removed from orders entirely, if required." } What's this? = render :partial => 'spree/admin/shared/order_sub_menu' -%div{ ng: { app: 'admin.lineItems', controller: 'LineItemsCtrl' } } +%div{ ng: { controller: 'LineItemsCtrl' } } %save-bar{ save: "submit()", form: "bulk_order_form" } .filters{ :class => "sixteen columns alpha" } .date_filter{ :class => "two columns alpha" } @@ -89,7 +92,7 @@ %div{ :class => "sixteen columns alpha", 'ng-show' => '!RequestMonitor.loading && filteredLineItems.length == 0'} %h1#no_results No orders found. - %div{ 'ng-hide' => 'RequestMonitor.loading || filteredLineItems.length == 0' } + .margin-bottom-50{ 'ng-hide' => 'RequestMonitor.loading || filteredLineItems.length == 0' } %form{ name: 'bulk_order_form' } %table.index#listing_orders.bulk{ :class => "sixteen columns alpha" } %thead diff --git a/app/views/spree/admin/overview/_enterprises.html.haml b/app/views/spree/admin/overview/_enterprises.html.haml index 757755c718..fbf19fbd43 100644 --- a/app/views/spree/admin/overview/_enterprises.html.haml +++ b/app/views/spree/admin/overview/_enterprises.html.haml @@ -1,4 +1,4 @@ -%div.dashboard_item.sixteen.columns.alpha#enterprises{ 'ng-app' => 'ofn.admin', 'ng-controller' => "enterprisesDashboardCtrl" } +%div.dashboard_item.sixteen.columns.alpha#enterprises{ 'ng-controller' => "enterprisesDashboardCtrl" } = render 'enterprises_header' - if @enterprises.empty? diff --git a/app/views/spree/admin/overview/_enterprises_header.html.haml b/app/views/spree/admin/overview/_enterprises_header.html.haml index fcc7d269f2..d198e8b549 100644 --- a/app/views/spree/admin/overview/_enterprises_header.html.haml +++ b/app/views/spree/admin/overview/_enterprises_header.html.haml @@ -5,4 +5,4 @@ %a.three.columns.omega.icon-plus.button.blue.white-bottom{ href: "#{main_app.new_admin_enterprise_path}" } CREATE NEW - else - %a.with-tip{ title: "Enterprises are Producers and/or Hubs and are the basic unit of organisation within the Open Food Network." } What's this? + %a{ "ofn-with-tip" => "Enterprises are Producers and/or Hubs and are the basic unit of organisation within the Open Food Network." } What's this? diff --git a/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml index cb177f9fb3..78c0e2427b 100644 --- a/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml +++ b/app/views/spree/admin/overview/_enterprises_hubs_tab.html.haml @@ -16,27 +16,27 @@ - if can? :admin, Spree::PaymentMethod - payment_method_count = enterprise.payment_methods.count - if payment_method_count > 0 - %span.icon-ok-sign.with-tip{ title: "#{pluralize payment_method_count, 'payment method'}" } + %span.icon-ok-sign{ 'ofn-with-tip' => "#{pluralize payment_method_count, 'payment method'}" } - else - %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no payment methods" } + %span.icon-remove-sign{ 'ofn-with-tip' => "#{enterprise.name} has no payment methods" } - else   %span.symbol.three.columns.centered - if can? :admin, Spree::ShippingMethod - shipping_method_count = enterprise.shipping_methods.count - if shipping_method_count > 0 - %span.icon-ok-sign.with-tip{ title: "#{pluralize shipping_method_count, 'shipping method'}" } + %span.icon-ok-sign{ 'ofn-with-tip' => "#{pluralize shipping_method_count, 'shipping method'}" } - else - %span.icon-remove-sign.with-tip{ title: "#{enterprise.name} has no shipping methods" } + %span.icon-remove-sign{ 'ofn-with-tip' => "#{enterprise.name} has no shipping methods" } - else   %span.symbol.three.columns.centered - if can? :admin, EnterpriseFee - fee_count = enterprise.enterprise_fees.count - if fee_count > 0 - %span.icon-ok-sign.with-tip{ title: "#{pluralize fee_count, 'fee'}" } + %span.icon-ok-sign{ 'ofn-with-tip' => "#{pluralize fee_count, 'fee'}" } - else - %span.icon-warning-sign.with-tip{ title: "#{enterprise.name} has no enterprise fees" } + %span.icon-warning-sign{ 'ofn-with-tip' => "#{enterprise.name} has no enterprise fees" } - else   %span.two.columns.omega.right diff --git a/app/views/spree/admin/overview/_order_cycles.html.haml b/app/views/spree/admin/overview/_order_cycles.html.haml index c1b7f90276..34b82c1f87 100644 --- a/app/views/spree/admin/overview/_order_cycles.html.haml +++ b/app/views/spree/admin/overview/_order_cycles.html.haml @@ -5,7 +5,7 @@ %a.three.columns.omega.icon-plus.button.blue{ href: "#{main_app.new_admin_order_cycle_path}" } CREATE NEW - else - %a.with-tip{ title: "Order cycles determine when and where your products are available to customers." } What's this? + %a{ "ofn-with-tip" => "Order cycles determine when and where your products are available to customers." } What's this? %div.seven.columns.alpha.list - if @order_cycle_count > 0 %div.seven.columns.alpha.list-item @@ -23,4 +23,4 @@ %span.icon-warning-sign %a.seven.columns.alpha.button.bottom.orange{ href: "#{main_app.admin_order_cycles_path}" } MANAGE ORDER CYCLES - %span.icon-arrow-right \ No newline at end of file + %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/_products.html.haml b/app/views/spree/admin/overview/_products.html.haml index 988e779398..24b60250c3 100644 --- a/app/views/spree/admin/overview/_products.html.haml +++ b/app/views/spree/admin/overview/_products.html.haml @@ -5,7 +5,7 @@ %a.three.columns.omega.icon-plus.button.blue{ href: "#{new_admin_product_path}" } CREATE NEW - else - %a.with-tip{ title: "The products that you sell through the Open Food Network." } What's this? + %a{ "ofn-with-tip" => "The products that you sell through the Open Food Network." } What's this? %div.seven.columns.alpha.list - if @product_count > 0 %div.seven.columns.alpha.list-item @@ -23,4 +23,4 @@ %span.icon-remove-sign %a.seven.columns.alpha.button.bottom.red{ href: "#{new_admin_product_path}" } CREATE A NEW PRODUCT - %span.icon-arrow-right \ No newline at end of file + %span.icon-arrow-right diff --git a/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml b/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml index 420196d3ce..511718d3f4 100644 --- a/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml +++ b/app/views/spree/admin/overview/multi_enterprise_dashboard.html.haml @@ -2,27 +2,28 @@ = render 'admin/shared/user_guide_link' -%h1{ :style => 'margin-bottom: 30px'} Dashboard +%div{ 'ng-app' => 'ofn.admin' } + %h1{ :style => 'margin-bottom: 30px' } Dashboard -- if @enterprises.unconfirmed.any? + - if @enterprises.unconfirmed.any? - = render partial: "unconfirmed" + = render partial: "unconfirmed" - %hr + %hr -- if @enterprises.empty? + - if @enterprises.empty? - = render partial: "enterprises" + = render partial: "enterprises" -- else + - else - - if can? :admin, Spree::Product - = render partial: "products" + - if can? :admin, Spree::Product + = render partial: "products" - %div.two.columns -   + %div.two.columns +   - - if can? :admin, OrderCycle - = render partial: "order_cycles" + - if can? :admin, OrderCycle + = render partial: "order_cycles" - = render partial: "enterprises" + = render partial: "enterprises" diff --git a/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml b/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml index ff345cb259..fb68704b79 100644 --- a/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml +++ b/app/views/spree/admin/products/bulk_edit/_products_variant.html.haml @@ -25,6 +25,6 @@ %td.actions %a{ 'ng-click' => 'editWarn(product,variant)', :class => "edit-variant icon-edit no-text", 'ng-show' => "variantSaved(variant)" } %td.actions - %span.icon-warning-sign.with-tip{ 'ng-if' => 'variant.variant_overrides', title: "This variant has {{variant.variant_overrides.length}} override(s)" } + %span.icon-warning-sign{ 'ng-if' => 'variant.variant_overrides', 'ofn-with-tip' => "This variant has {{variant.variant_overrides.length}} override(s)" } %td.actions %a{ 'ng-click' => 'deleteVariant(product,variant)', "ng-class" => '{disabled: product.variants.length < 2}', :class => "delete-variant icon-trash no-text" } diff --git a/config/application.rb b/config/application.rb index ba75a097ec..1fb6c88bbf 100644 --- a/config/application.rb +++ b/config/application.rb @@ -102,5 +102,6 @@ module Openfoodnetwork config.assets.precompile += ['search/all.css', 'search/*.js'] config.assets.precompile += ['shared/*'] + config.active_support.escape_html_entities_in_json = true end end diff --git a/config/application.yml.example b/config/application.yml.example index 45fface302..0b1871466b 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -13,3 +13,11 @@ LOCALE: en CHECKOUT_ZONE: Australia # Find currency codes at http://en.wikipedia.org/wiki/ISO_4217. CURRENCY: AUD + +# SingleSignOn login for Discourse +# +# DISCOURSE_SSO_SECRET should be a random string. It must be the same as provided to your Discourse instance. +#DISCOURSE_SSO_SECRET: "" +# +# DISCOURSE_URL must be the URL of your Discourse instance. +#DISCOURSE_URL: "https://noticeboard.openfoodnetwork.org.au" diff --git a/config/locales/en.yml b/config/locales/en.yml index fb79dfe1fc..7f812363c4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -58,6 +58,53 @@ en: sort_order_cycles_on_shopfront_by: "Sort Order Cycles On Shopfront By" + + admin: + # General form elements + quick_search: Quick Search + clear_all: Clear All + producer: Producer + shop: Shop + product: Product + variant: Variant + + columns: Columns + actions: Actions + viewing: "Viewing: %{current_view_name}" + + whats_this: What's this? + + inventory: + title: Inventory + description: Use this page to manage inventories for your enterprises. Any product details set here will override those set on the 'Products' page + sku: SKU + price: Price + on_hand: On Hand + on_demand: On Demand? + enable_reset: Enable Stock Level Reset? + inherit: Inherit? + add: Add + hide: Hide + select_a_shop: Select A Shop + review_now: Review Now + new_products_alert_message: There are %{new_product_count} new products available to add to your inventory. + currently_empty: Your inventory is currently empty + no_matching_products: No matching products found in your inventory + no_hidden_products: No products have been hidden from this inventory + no_matching_hidden_products: No hidden products match your search criteria + no_new_products: No new products are available to add to this inventory + no_matching_new_products: No new products match your search criteria + inventory_powertip: This is your inventory of products. To add products to your inventory, select 'New Products' from the Viewing dropdown. + hidden_powertip: These products have been hidden from your inventory and will not be available to add to your shop. You can click 'Add' to add a product to you inventory. + new_powertip: These products are available to be added to your inventory. Click 'Add' to add a product to your inventory, or 'Hide' to hide it from view. You can always change your mind later! + + + order_cycle: + choose_products_from: "Choose Products From:" + + enterprise: + select_outgoing_oc_products_from: Select outgoing OC products from + # Printable Invoice Columns invoice_column_item: "Item" invoice_column_qty: "Qty" @@ -114,6 +161,7 @@ en: label_account: "Account" label_more: "More" label_less: "Show less" + label_notices: "Notices" items: "items" cart_headline: "Your shopping cart" diff --git a/config/routes.rb b/config/routes.rb index 882dfec23d..9821a8b4bf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -11,6 +11,9 @@ Openfoodnetwork::Application.routes.draw do get "/#/login", to: "home#index", as: :spree_login get "/login", to: redirect("/#/login") + get "/discourse/login", to: "discourse_sso#login" + get "/discourse/sso", to: "discourse_sso#sso" + get "/map", to: "map#index", as: :map get "/register", to: "registration#index", as: :registration @@ -101,11 +104,15 @@ Openfoodnetwork::Application.routes.draw do get :move_down end + get '/inventory', to: 'variant_overrides#index' + resources :variant_overrides do post :bulk_update, on: :collection post :bulk_reset, on: :collection end + resources :inventory_items, only: [:create, :update] + resources :customers, only: [:index, :update] resource :content diff --git a/db/migrate/20160114001844_create_inventory_items.rb b/db/migrate/20160114001844_create_inventory_items.rb new file mode 100644 index 0000000000..2ca54dce4d --- /dev/null +++ b/db/migrate/20160114001844_create_inventory_items.rb @@ -0,0 +1,13 @@ +class CreateInventoryItems < ActiveRecord::Migration + def change + create_table :inventory_items do |t| + t.references :enterprise, null: false, index: true + t.references :variant, null: false, index: true + t.boolean :visible, default: true, null: false + + t.timestamps + end + + add_index "inventory_items", [:enterprise_id, :variant_id], unique: true + end +end diff --git a/db/migrate/20160204031816_add_inherits_tax_category_to_enterprise_fees.rb b/db/migrate/20160204031816_add_inherits_tax_category_to_enterprise_fees.rb new file mode 100644 index 0000000000..49751cc479 --- /dev/null +++ b/db/migrate/20160204031816_add_inherits_tax_category_to_enterprise_fees.rb @@ -0,0 +1,5 @@ +class AddInheritsTaxCategoryToEnterpriseFees < ActiveRecord::Migration + def change + add_column :enterprise_fees, :inherits_tax_category, :boolean, null: false, default: false + end +end diff --git a/db/migrate/20160218235221_populate_inventories.rb b/db/migrate/20160218235221_populate_inventories.rb new file mode 100644 index 0000000000..2b9d1be8b6 --- /dev/null +++ b/db/migrate/20160218235221_populate_inventories.rb @@ -0,0 +1,22 @@ +class PopulateInventories < ActiveRecord::Migration + def up + # If hubs are actively using overrides, populate their inventories with all variants they have permission to override + # Otherwise leave their inventories empty + + hubs_using_overrides = Enterprise.joins("LEFT OUTER JOIN variant_overrides ON variant_overrides.hub_id = enterprises.id") + .where("variant_overrides.id IS NOT NULL").select("DISTINCT enterprises.*") + + hubs_using_overrides.each do |hub| + overridable_producers = OpenFoodNetwork::Permissions.new(hub.owner).variant_override_producers + + variants = Spree::Variant.where(is_master: false, product_id: Spree::Product.not_deleted.where(supplier_id: overridable_producers)) + + variants.each do |variant| + InventoryItem.create(enterprise: hub, variant: variant, visible: true) + end + end + end + + def down + end +end diff --git a/db/migrate/20160224034034_grant_explicit_variant_override_permissions.rb b/db/migrate/20160224034034_grant_explicit_variant_override_permissions.rb new file mode 100644 index 0000000000..b56a4a8a93 --- /dev/null +++ b/db/migrate/20160224034034_grant_explicit_variant_override_permissions.rb @@ -0,0 +1,30 @@ +class GrantExplicitVariantOverridePermissions < ActiveRecord::Migration + def up + hubs = Enterprise.is_distributor + + begin + EnterpriseRelationship.skip_callback :save, :after, :apply_variant_override_permissions + + hubs.each do |hub| + next if hub.owner.admin? + explicitly_granting_producer_ids = hub.relationships_as_child + .with_permission(:create_variant_overrides).map(&:parent_id) + + managed_producer_ids = Enterprise.managed_by(hub.owner).is_primary_producer.pluck(:id) + implicitly_granting_producer_ids = managed_producer_ids - explicitly_granting_producer_ids - [hub.id] + + # create explicit VO permissions for producers currently granting implicit permission + Enterprise.where(id: implicitly_granting_producer_ids).each do |producer| + relationship = producer.relationships_as_parent.find_or_initialize_by_child_id(hub.id) + permission = relationship.permissions.find_or_initialize_by_name(:create_variant_overrides) + relationship.save! unless permission.persisted? + end + end + ensure + EnterpriseRelationship.set_callback :save, :after, :apply_variant_override_permissions + end + end + + def down + end +end diff --git a/db/migrate/20160224230143_add_permission_revoked_at_to_variant_overrides.rb b/db/migrate/20160224230143_add_permission_revoked_at_to_variant_overrides.rb new file mode 100644 index 0000000000..afa58ebe26 --- /dev/null +++ b/db/migrate/20160224230143_add_permission_revoked_at_to_variant_overrides.rb @@ -0,0 +1,21 @@ +class AddPermissionRevokedAtToVariantOverrides < ActiveRecord::Migration + def up + add_column :variant_overrides, :permission_revoked_at, :datetime, default: nil + + variant_override_hubs = Enterprise.where(id: VariantOverride.all.map(&:hub_id).uniq) + + variant_override_hubs.each do |hub| + permitting_producer_ids = hub.relationships_as_child + .with_permission(:create_variant_overrides).map(&:parent_id) + + variant_overrides_with_revoked_permissions = VariantOverride.for_hubs(hub) + .joins(variant: :product).where("spree_products.supplier_id NOT IN (?)", permitting_producer_ids) + + variant_overrides_with_revoked_permissions.update_all(permission_revoked_at: Time.now) + end + end + + def down + remove_column :variant_overrides, :permission_revoked_at + end +end diff --git a/db/migrate/20160302044850_repopulate_inventories.rb b/db/migrate/20160302044850_repopulate_inventories.rb new file mode 100644 index 0000000000..e38628439a --- /dev/null +++ b/db/migrate/20160302044850_repopulate_inventories.rb @@ -0,0 +1,30 @@ +class RepopulateInventories < ActiveRecord::Migration + # Previous version of this migration (20160218235221) relied on Permissions#variant_override_producers + # which was then changed, meaning that an incomplete set of variants were added to inventories of most hubs + # Re-running this now will ensure that all permitted variants (including those allowed by 20160224034034) are + # added to the relevant inventories + + def up + # If hubs are actively using overrides, populate their inventories with all variants they have permission to override + # Otherwise leave their inventories empty + + hubs_using_overrides = Enterprise.joins("LEFT OUTER JOIN variant_overrides ON variant_overrides.hub_id = enterprises.id") + .where("variant_overrides.id IS NOT NULL").select("DISTINCT enterprises.*") + + hubs_using_overrides.each do |hub| + overridable_producer_ids = hub.relationships_as_child.with_permission(:create_variant_overrides).map(&:parent_id) | [hub.id] + + variants = Spree::Variant.where(is_master: false, product_id: Spree::Product.not_deleted.where(supplier_id: overridable_producer_ids)) + + variants_to_add = variants.joins("LEFT OUTER JOIN (SELECT * from inventory_items WHERE enterprise_id = #{hub.id}) AS o_inventory_items ON o_inventory_items.variant_id = spree_variants.id") + .where('o_inventory_items.id IS NULL') + + variants_to_add.each do |variant| + inventory_item = InventoryItem.create(enterprise: hub, variant: variant, visible: true) + end + end + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index 465530b949..5696e5168a 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 => 20151128185900) do +ActiveRecord::Schema.define(:version => 20160302044850) do create_table "account_invoices", :force => true do |t| t.integer "user_id", :null => false @@ -235,9 +235,10 @@ ActiveRecord::Schema.define(:version => 20151128185900) 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" + t.boolean "inherits_tax_category", :default => false, :null => false end add_index "enterprise_fees", ["enterprise_id"], :name => "index_enterprise_fees_on_enterprise_id" @@ -394,6 +395,16 @@ ActiveRecord::Schema.define(:version => 20151128185900) do add_index "exchanges", ["receiver_id"], :name => "index_exchanges_on_receiver_id" add_index "exchanges", ["sender_id"], :name => "index_exchanges_on_sender_id" + create_table "inventory_items", :force => true do |t| + t.integer "enterprise_id", :null => false + t.integer "variant_id", :null => false + t.boolean "visible", :default => true, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "inventory_items", ["enterprise_id", "variant_id"], :name => "index_inventory_items_on_enterprise_id_and_variant_id", :unique => true + create_table "order_cycles", :force => true do |t| t.string "name" t.datetime "orders_open_at" @@ -670,9 +681,9 @@ ActiveRecord::Schema.define(:version => 20151128185900) 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" t.integer "customer_id" end @@ -1155,14 +1166,15 @@ ActiveRecord::Schema.define(:version => 20151128185900) do add_index "tags", ["name"], :name => "index_tags_on_name", :unique => true create_table "variant_overrides", :force => true do |t| - t.integer "variant_id", :null => false - t.integer "hub_id", :null => false - t.decimal "price", :precision => 8, :scale => 2 - t.integer "count_on_hand" - t.integer "default_stock" - t.boolean "resettable" - t.string "sku" - t.boolean "on_demand" + t.integer "variant_id", :null => false + t.integer "hub_id", :null => false + t.decimal "price", :precision => 8, :scale => 2 + t.integer "count_on_hand" + t.integer "default_stock" + t.boolean "resettable" + t.string "sku" + t.boolean "on_demand" + t.datetime "permission_revoked_at" end add_index "variant_overrides", ["variant_id", "hub_id"], :name => "index_variant_overrides_on_variant_id_and_hub_id" diff --git a/lib/discourse/single_sign_on.rb b/lib/discourse/single_sign_on.rb new file mode 100644 index 0000000000..046a2d677c --- /dev/null +++ b/lib/discourse/single_sign_on.rb @@ -0,0 +1,107 @@ +# This class is the reference implementation of a SSO provider from Discourse. + +module Discourse + class SingleSignOn + ACCESSORS = [:nonce, :name, :username, :email, :avatar_url, :avatar_force_update, :require_activation, + :about_me, :external_id, :return_sso_url, :admin, :moderator, :suppress_welcome_message] + FIXNUMS = [] + BOOLS = [:avatar_force_update, :admin, :moderator, :require_activation, :suppress_welcome_message] + NONCE_EXPIRY_TIME = 10.minutes + + attr_accessor(*ACCESSORS) + attr_accessor :sso_secret, :sso_url + + def self.sso_secret + raise RuntimeError, "sso_secret not implemented on class, be sure to set it on instance" + end + + def self.sso_url + raise RuntimeError, "sso_url not implemented on class, be sure to set it on instance" + end + + def self.parse(payload, sso_secret = nil) + sso = new + sso.sso_secret = sso_secret if sso_secret + + parsed = Rack::Utils.parse_query(payload) + if sso.sign(parsed["sso"]) != parsed["sig"] + diags = "\n\nsso: #{parsed["sso"]}\n\nsig: #{parsed["sig"]}\n\nexpected sig: #{sso.sign(parsed["sso"])}" + if parsed["sso"] =~ /[^a-zA-Z0-9=\r\n\/+]/m + raise RuntimeError, "The SSO field should be Base64 encoded, using only A-Z, a-z, 0-9, +, /, and = characters. Your input contains characters we don't understand as Base64, see http://en.wikipedia.org/wiki/Base64 #{diags}" + else + raise RuntimeError, "Bad signature for payload #{diags}" + end + end + + decoded = Base64.decode64(parsed["sso"]) + decoded_hash = Rack::Utils.parse_query(decoded) + + ACCESSORS.each do |k| + val = decoded_hash[k.to_s] + val = val.to_i if FIXNUMS.include? k + if BOOLS.include? k + val = ["true", "false"].include?(val) ? val == "true" : nil + end + sso.send("#{k}=", val) + end + + decoded_hash.each do |k,v| + # 1234567 + # custom. + # + if k[0..6] == "custom." + field = k[7..-1] + sso.custom_fields[field] = v + end + end + + sso + end + + def sso_secret + @sso_secret || self.class.sso_secret + end + + def sso_url + @sso_url || self.class.sso_url + end + + def custom_fields + @custom_fields ||= {} + end + + + def sign(payload) + OpenSSL::HMAC.hexdigest("sha256", sso_secret, payload) + end + + + def to_url(base_url=nil) + base = "#{base_url || sso_url}" + "#{base}#{base.include?('?') ? '&' : '?'}#{payload}" + end + + def payload + payload = Base64.encode64(unsigned_payload) + "sso=#{CGI::escape(payload)}&sig=#{sign(payload)}" + end + + def unsigned_payload + payload = {} + ACCESSORS.each do |k| + next if (val = send k) == nil + + payload[k] = val + end + + if @custom_fields + @custom_fields.each do |k,v| + payload["custom.#{k}"] = v.to_s + end + end + + Rack::Utils.build_query(payload) + end + + end +end diff --git a/lib/open_food_network/enterprise_fee_applicator.rb b/lib/open_food_network/enterprise_fee_applicator.rb index 4962bc148e..06e71dd21c 100644 --- a/lib/open_food_network/enterprise_fee_applicator.rb +++ b/lib/open_food_network/enterprise_fee_applicator.rb @@ -5,7 +5,7 @@ module OpenFoodNetwork 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) + a.set_absolute_included_tax! adjustment_tax(line_item, a) end def create_order_adjustment(order) @@ -31,12 +31,22 @@ module OpenFoodNetwork "#{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) : [] + def adjustment_tax(adjustable, adjustment) + tax_rates = rates_for(adjustable) - tax_rates.sum do |rate| + tax_rates.select(&:included_in_price).sum do |rate| rate.compute_tax adjustment.amount end end + + def rates_for(adjustable) + case adjustable + when Spree::LineItem + tax_category = enterprise_fee.inherits_tax_category? ? adjustable.product.tax_category : enterprise_fee.tax_category + return tax_category ? tax_category.tax_rates.match(adjustable.order) : [] + when Spree::Order + return enterprise_fee.tax_category ? enterprise_fee.tax_category.tax_rates.match(adjustable) : [] + end + end end end diff --git a/lib/open_food_network/enterprise_fee_calculator.rb b/lib/open_food_network/enterprise_fee_calculator.rb index d65a80c0dc..7222abad1c 100644 --- a/lib/open_food_network/enterprise_fee_calculator.rb +++ b/lib/open_food_network/enterprise_fee_calculator.rb @@ -112,6 +112,8 @@ module OpenFoodNetwork def per_item_enterprise_fee_applicators_for(variant) fees = [] + return [] unless @order_cycle && @distributor + @order_cycle.exchanges_carrying(variant, @distributor).each do |exchange| exchange.enterprise_fees.per_item.each do |enterprise_fee| fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, exchange.role) @@ -128,6 +130,8 @@ module OpenFoodNetwork def per_order_enterprise_fee_applicators_for(order) fees = [] + return fees unless @order_cycle && order.distributor + @order_cycle.exchanges_supplying(order).each do |exchange| exchange.enterprise_fees.per_order.each do |enterprise_fee| fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, exchange.role) diff --git a/lib/open_food_network/order_cycle_form_applicator.rb b/lib/open_food_network/order_cycle_form_applicator.rb index 3e81d6c9f1..6d2e3e3e53 100644 --- a/lib/open_food_network/order_cycle_form_applicator.rb +++ b/lib/open_food_network/order_cycle_form_applicator.rb @@ -140,8 +140,13 @@ module OpenFoodNetwork end def persisted_variants_hash(exchange) - exchange ||= OpenStruct.new(variants: []) - Hash[ exchange.variants.map{ |v| [v.id, true] } ] + return {} unless exchange + + # When we have permission to edit a variant, mark it for removal here, assuming it will be included again if that is what the use wants + # When we don't have permission to edit a variant and it is already in the exchange, keep it in the exchange. + method_name = "editable_variant_ids_for_#{ exchange.incoming? ? 'incoming' : 'outgoing' }_exchange_between" + editable = send(method_name, exchange.sender, exchange.receiver) + Hash[ exchange.variants.map { |v| [v.id, editable.exclude?(v.id)] } ] end def incoming_exchange_variant_ids(attrs) diff --git a/lib/open_food_network/permissions.rb b/lib/open_food_network/permissions.rb index 2e6329bd4b..795bd3cf68 100644 --- a/lib/open_food_network/permissions.rb +++ b/lib/open_food_network/permissions.rb @@ -26,7 +26,7 @@ module OpenFoodNetwork end def variant_override_hubs - managed_and_related_enterprises_granting(:add_to_order_cycle).is_hub + managed_enterprises.is_distributor end def variant_override_producers @@ -38,7 +38,7 @@ module OpenFoodNetwork # override variants # {hub1_id => [producer1_id, producer2_id, ...], ...} def variant_override_enterprises_per_hub - hubs = managed_and_related_enterprises_granting(:add_to_order_cycle).is_distributor + hubs = variant_override_hubs # Permissions granted by create_variant_overrides relationship from producer to hub permissions = Hash[ @@ -49,13 +49,10 @@ module OpenFoodNetwork map { |child_id, ers| [child_id, ers.map { |er| er.parent_id }] } ] - # We have permission to create variant overrides for any producers we manage, for any - # hub we can add to an order cycle - managed_producer_ids = managed_enterprises.is_primary_producer.pluck(:id) - if managed_producer_ids.any? - hubs.each do |hub| - permissions[hub.id] = ((permissions[hub.id] || []) + managed_producer_ids).uniq - end + # Allow a producer hub to override it's own products without explicit permission + hubs.is_primary_producer.each do |hub| + permissions[hub.id] ||= [] + permissions[hub.id] |= [hub.id] end permissions diff --git a/lib/open_food_network/products_renderer.rb b/lib/open_food_network/products_renderer.rb new file mode 100644 index 0000000000..d745b1b794 --- /dev/null +++ b/lib/open_food_network/products_renderer.rb @@ -0,0 +1,84 @@ +require 'open_food_network/scope_product_to_hub' + +module OpenFoodNetwork + class ProductsRenderer + class NoProducts < Exception; end + + def initialize(distributor, order_cycle) + @distributor = distributor + @order_cycle = order_cycle + end + + def products + products = products_for_shop + + if products + enterprise_fee_calculator = EnterpriseFeeCalculator.new @distributor, @order_cycle + + ActiveModel::ArraySerializer.new(products, + each_serializer: Api::ProductSerializer, + current_order_cycle: @order_cycle, + current_distributor: @distributor, + variants: variants_for_shop_by_id, + master_variants: master_variants_for_shop_by_id, + enterprise_fee_calculator: enterprise_fee_calculator, + ).to_json + else + raise NoProducts.new + end + end + + + private + + def products_for_shop + if @order_cycle + scoper = ScopeProductToHub.new(@distributor) + + @order_cycle. + valid_products_distributed_by(@distributor). + order(taxon_order). + each { |p| scoper.scope(p) }. + select { |p| !p.deleted? && p.has_stock_for_distribution?(@order_cycle, @distributor) } + end + end + + def taxon_order + if @distributor.preferred_shopfront_taxon_order.present? + @distributor + .preferred_shopfront_taxon_order + .split(",").map { |id| "primary_taxon_id=#{id} DESC" } + .join(",") + ", name ASC" + else + "name ASC" + end + end + + def all_variants_for_shop + # We use the in_stock? method here instead of the in_stock scope because we need to + # look up the stock as overridden by VariantOverrides, and the scope method is not affected + # by them. + scoper = OpenFoodNetwork::ScopeVariantToHub.new(@distributor) + Spree::Variant. + for_distribution(@order_cycle, @distributor). + each { |v| scoper.scope(v) }. + select(&:in_stock?) + end + + def variants_for_shop_by_id + index_by_product_id all_variants_for_shop.reject(&:is_master) + end + + def master_variants_for_shop_by_id + index_by_product_id all_variants_for_shop.select(&:is_master) + end + + def index_by_product_id(variants) + variants.inject({}) do |vs, v| + vs[v.product_id] ||= [] + vs[v.product_id] << v + vs + end + end + end +end diff --git a/spec/controllers/admin/inventory_items_controller_spec.rb b/spec/controllers/admin/inventory_items_controller_spec.rb new file mode 100644 index 0000000000..4828f5c662 --- /dev/null +++ b/spec/controllers/admin/inventory_items_controller_spec.rb @@ -0,0 +1,129 @@ +require 'spec_helper' + +describe Admin::InventoryItemsController, type: :controller do + # include AuthenticationWorkflow + + describe "create" do + context "json" do + let(:format) { :json } + + let(:enterprise) { create(:distributor_enterprise) } + let(:variant) { create(:variant) } + let(:inventory_item) { create(:inventory_item, enterprise: enterprise, variant: variant, visible: true) } + let(:params) { { format: format, inventory_item: { enterprise_id: enterprise.id, variant_id: variant.id, visible: false } } } + + context "where I don't manage the inventory item enterprise" do + before do + user = create(:user) + user.owned_enterprises << create(:enterprise) + allow(controller).to receive(:spree_current_user) { user } + end + + it "redirects to unauthorized" do + spree_post :create, params + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "where I manage the variant override hub" do + before do + allow(controller).to receive(:spree_current_user) { enterprise.owner } + end + + context "but the producer has not granted VO permission" do + it "redirects to unauthorized" do + spree_post :create, params + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "and the producer has granted VO permission" do + before do + create(:enterprise_relationship, parent: variant.product.supplier, child: enterprise, permissions_list: [:create_variant_overrides]) + end + + context "with acceptable data" do + it "allows me to create the inventory item" do + expect{ spree_post :create, params }.to change{InventoryItem.count}.by(1) + inventory_item = InventoryItem.last + expect(inventory_item.enterprise).to eq enterprise + expect(inventory_item.variant).to eq variant + expect(inventory_item.visible).to be false + end + end + + context "with unacceptable data" do + render_views + let!(:bad_params) { { format: format, inventory_item: { enterprise_id: enterprise.id, variant_id: variant.id, visible: nil } } } + + it "returns an error message" do + expect{ spree_post :create, bad_params }.to change{InventoryItem.count}.by(0) + expect(response.body).to eq Hash[:errors, ["Visible must be true or false"]].to_json + end + end + end + end + end + end + + describe "update" do + context "json" do + let(:format) { :json } + + let(:enterprise) { create(:distributor_enterprise) } + let(:variant) { create(:variant) } + let(:inventory_item) { create(:inventory_item, enterprise: enterprise, variant: variant, visible: true) } + let(:params) { { format: format, id: inventory_item.id, inventory_item: { visible: false } } } + + context "where I don't manage the inventory item enterprise" do + before do + user = create(:user) + user.owned_enterprises << create(:enterprise) + allow(controller).to receive(:spree_current_user) { user } + end + + it "redirects to unauthorized" do + spree_put :update, params + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "where I manage the variant override hub" do + before do + allow(controller).to receive(:spree_current_user) { enterprise.owner } + end + + context "but the producer has not granted VO permission" do + it "redirects to unauthorized" do + spree_put :update, params + expect(response).to redirect_to spree.unauthorized_path + end + end + + context "and the producer has granted VO permission" do + before do + create(:enterprise_relationship, parent: variant.product.supplier, child: enterprise, permissions_list: [:create_variant_overrides]) + end + + context "with acceptable data" do + it "allows me to update the inventory item" do + spree_put :update, params + inventory_item.reload + expect(inventory_item.visible).to eq false + end + end + + context "with unacceptable data" do + render_views + let!(:bad_params) { { format: format, id: inventory_item.id, inventory_item: { visible: nil } } } + + it "returns an error message" do + expect{ spree_put :update, bad_params }.to change{InventoryItem.count}.by(0) + expect(response.body).to eq Hash[:errors, ["Visible must be true or false"]].to_json + end + end + 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 index 9fc9ad9096..3cc29193ab 100644 --- a/spec/controllers/admin/order_cycles_controller_spec.rb +++ b/spec/controllers/admin/order_cycles_controller_spec.rb @@ -103,10 +103,34 @@ module Admin end it "does not set flash message otherwise" do - spree_put :update, id: order_cycle.id, reloading: '0', order_cycle: {} flash[:notice].should be_nil end + context "when updating without explicitly submitting exchanges" do + let(:form_applicator_mock) { double(:form_applicator) } + let(:incoming_exchange) { create(:exchange, order_cycle: order_cycle, incoming: true) } + let(:outgoing_exchange) { create(:exchange, order_cycle: order_cycle, incoming: false) } + + + before do + allow(OpenFoodNetwork::OrderCycleFormApplicator).to receive(:new) { form_applicator_mock } + allow(form_applicator_mock).to receive(:go!) { nil } + end + + it "does not run the OrderCycleFormApplicator" do + expect(order_cycle.exchanges.incoming).to eq [incoming_exchange] + expect(order_cycle.exchanges.outgoing).to eq [outgoing_exchange] + expect(order_cycle.prefers_product_selection_from_coordinator_inventory_only?).to be false + spree_put :update, id: order_cycle.id, order_cycle: { name: 'Some new name', preferred_product_selection_from_coordinator_inventory_only: true } + expect(form_applicator_mock).to_not have_received(:go!) + order_cycle.reload + expect(order_cycle.exchanges.incoming).to eq [incoming_exchange] + expect(order_cycle.exchanges.outgoing).to eq [outgoing_exchange] + expect(order_cycle.name).to eq 'Some new name' + expect(order_cycle.prefers_product_selection_from_coordinator_inventory_only?).to be true + end + end + context "as a producer supplying to an order cycle" do let(:producer) { create(:supplier_enterprise) } let(:coordinator) { order_cycle.coordinator } diff --git a/spec/controllers/admin/variant_overrides_controller_spec.rb b/spec/controllers/admin/variant_overrides_controller_spec.rb index d796f2d52f..3bd2632979 100644 --- a/spec/controllers/admin/variant_overrides_controller_spec.rb +++ b/spec/controllers/admin/variant_overrides_controller_spec.rb @@ -9,6 +9,7 @@ describe Admin::VariantOverridesController, type: :controller do let(:hub) { create(:distributor_enterprise) } let(:variant) { create(:variant) } + let!(:inventory_item) { create(:inventory_item, enterprise: hub, variant: variant, visible: true) } let!(:variant_override) { create(:variant_override, hub: hub, variant: variant) } let(:variant_override_params) { [ { id: variant_override.id, price: 123.45, count_on_hand: 321, sku: "MySKU", on_demand: false } ] } @@ -42,6 +43,14 @@ describe Admin::VariantOverridesController, type: :controller do create(:enterprise_relationship, parent: variant.product.supplier, child: hub, permissions_list: [:create_variant_overrides]) end + it "loads data" do + spree_put :bulk_update, format: format, variant_overrides: variant_override_params + expect(assigns[:hubs]).to eq [hub] + expect(assigns[:producers]).to eq [variant.product.supplier] + expect(assigns[:hub_permissions]).to eq Hash[hub.id,[variant.product.supplier.id]] + expect(assigns[:inventory_items]).to eq [inventory_item] + end + it "allows me to update the variant override" do spree_put :bulk_update, format: format, variant_overrides: variant_override_params variant_override.reload @@ -106,6 +115,14 @@ describe Admin::VariantOverridesController, type: :controller do context "where the producer has granted create_variant_overrides permission to the hub" do let!(:er1) { create(:enterprise_relationship, parent: producer, child: hub, permissions_list: [:create_variant_overrides]) } + it "loads data" do + spree_put :bulk_reset, params + expect(assigns[:hubs]).to eq [hub] + expect(assigns[:producers]).to eq [producer] + expect(assigns[:hub_permissions]).to eq Hash[hub.id,[producer.id]] + expect(assigns[:inventory_items]).to eq [] + end + it "updates stock to default values where reset is enabled" do expect(variant_override1.reload.count_on_hand).to eq 5 # reset enabled expect(variant_override2.reload.count_on_hand).to eq 2 # reset disabled diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index 2c7105b356..09ea85dd44 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -37,7 +37,7 @@ describe ShopController do controller.current_order_cycle.should == oc2 end - context "RABL tests" do + context "JSON tests" do render_views it "should return the order cycle details when the oc is selected" do oc1 = create(:simple_order_cycle, distributors: [d]) @@ -86,7 +86,7 @@ describe ShopController do describe "requests and responses" do let(:product) { create(:product) } before do - exchange.variants << product.master + exchange.variants << product.variants.first end it "returns products via json" do @@ -102,95 +102,6 @@ describe ShopController do response.body.should be_empty end end - - describe "sorting" do - let(:t1) { create(:taxon) } - let(:t2) { create(:taxon) } - let!(:p1) { create(:product, name: "abc", primary_taxon_id: t2.id) } - let!(:p2) { create(:product, name: "def", primary_taxon_id: t1.id) } - let!(:p3) { create(:product, name: "ghi", primary_taxon_id: t2.id) } - let!(:p4) { create(:product, name: "jkl", primary_taxon_id: t1.id) } - - before do - exchange.variants << p1.variants.first - exchange.variants << p2.variants.first - exchange.variants << p3.variants.first - exchange.variants << p4.variants.first - end - - it "sorts products by the distributor's preferred taxon list" do - d.stub(:preferred_shopfront_taxon_order) {"#{t1.id},#{t2.id}"} - controller.stub(:current_order_cycle).and_return order_cycle - xhr :get, :products - assigns[:products].should == [p2, p4, p1, p3] - end - - it "alphabetizes products by name when taxon list is not set" do - d.stub(:preferred_shopfront_taxon_order) {""} - controller.stub(:current_order_cycle).and_return order_cycle - xhr :get, :products - assigns[:products].should == [p1, p2, p3, p4] - end - end - - context "RABL tests" do - render_views - let(:product) { create(:product) } - let(:variant) { product.variants.first } - - before do - exchange.variants << variant - controller.stub(:current_order_cycle).and_return order_cycle - end - - it "only returns products for the current order cycle" do - xhr :get, :products - response.body.should have_content product.name - end - - it "doesn't return products not in stock" do - variant.update_attribute(:count_on_hand, 0) - xhr :get, :products - response.body.should_not have_content product.name - end - - it "strips html from description" do - product.update_attribute(:description, "turtles frogs") - xhr :get, :products - response.body.should have_content "frogs" - response.body.should_not have_content " [v1]} end end end diff --git a/spec/factories.rb b/spec/factories.rb index 2d3d3bbe88..67080827ab 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -98,6 +98,12 @@ FactoryGirl.define do resettable false end + factory :inventory_item, :class => InventoryItem do + enterprise + variant + visible true + end + factory :enterprise, :class => Enterprise do owner { FactoryGirl.create :user } sequence(:name) { |n| "Enterprise #{n}" } diff --git a/spec/features/admin/bulk_order_management_spec.rb b/spec/features/admin/bulk_order_management_spec.rb index 1e011b5939..767274b274 100644 --- a/spec/features/admin/bulk_order_management_spec.rb +++ b/spec/features/admin/bulk_order_management_spec.rb @@ -159,6 +159,7 @@ feature %q{ first("div#columns-dropdown", :text => "COLUMNS").click first("div#columns-dropdown div.menu div.menu_item", text: "Weight/Volume").click first("div#columns-dropdown div.menu div.menu_item", text: "Price").click + first("div#columns-dropdown", :text => "COLUMNS").click within "tr#li_#{li1.id}" do expect(page).to have_field "price", with: "$50.00" fill_in "final_weight_volume", :with => 2000 @@ -177,6 +178,7 @@ feature %q{ visit '/admin/orders/bulk_management' first("div#columns-dropdown", :text => "COLUMNS").click first("div#columns-dropdown div.menu div.menu_item", text: "Price").click + first("div#columns-dropdown", :text => "COLUMNS").click within "tr#li_#{li1.id}" do expect(page).to have_field "price", with: "$#{format("%.2f",li1.price * 5)}" fill_in "quantity", :with => 6 @@ -190,6 +192,7 @@ feature %q{ visit '/admin/orders/bulk_management' first("div#columns-dropdown", :text => "COLUMNS").click first("div#columns-dropdown div.menu div.menu_item", text: "Weight/Volume").click + first("div#columns-dropdown", :text => "COLUMNS").click within "tr#li_#{li1.id}" do expect(page).to have_field "final_weight_volume", with: "#{li1.final_weight_volume.round}" fill_in "quantity", :with => 6 @@ -211,6 +214,7 @@ feature %q{ first("div#columns-dropdown", :text => "COLUMNS").click first("div#columns-dropdown div.menu div.menu_item", text: "Producer").click + first("div#columns-dropdown", :text => "COLUMNS").click expect(page).to_not have_selector "th", :text => "PRODUCER" expect(page).to have_selector "th", :text => "NAME" @@ -236,9 +240,9 @@ feature %q{ it "displays a select box for producers, which filters line items by the selected supplier" do supplier_names = ["All"] Enterprise.is_primary_producer.each{ |e| supplier_names << e.name } - find("div.select2-container#s2id_supplier_filter").click + open_select2 "div.select2-container#s2id_supplier_filter" supplier_names.each { |sn| expect(page).to have_selector "div.select2-drop-active ul.select2-results li", text: sn } - find("div.select2-container#s2id_supplier_filter").click + close_select2 "div.select2-container#s2id_supplier_filter" expect(page).to have_selector "tr#li_#{li1.id}", visible: true expect(page).to have_selector "tr#li_#{li2.id}", visible: true select2_select s1.name, from: "supplier_filter" @@ -271,9 +275,9 @@ feature %q{ it "displays a select box for distributors, which filters line items by the selected distributor" do distributor_names = ["All"] Enterprise.is_distributor.each{ |e| distributor_names << e.name } - find("div.select2-container#s2id_distributor_filter").click + open_select2 "div.select2-container#s2id_distributor_filter" distributor_names.each { |dn| expect(page).to have_selector "div.select2-drop-active ul.select2-results li", text: dn } - find("div.select2-container#s2id_distributor_filter").click + close_select2 "div.select2-container#s2id_distributor_filter" expect(page).to have_selector "tr#li_#{li1.id}", visible: true expect(page).to have_selector "tr#li_#{li2.id}", visible: true select2_select d1.name, from: "distributor_filter" diff --git a/spec/features/admin/enterprise_fees_spec.rb b/spec/features/admin/enterprise_fees_spec.rb index d462a3d809..7025b54766 100644 --- a/spec/features/admin/enterprise_fees_spec.rb +++ b/spec/features/admin/enterprise_fees_spec.rb @@ -17,11 +17,11 @@ feature %q{ click_link 'Configuration' click_link 'Enterprise Fees' - page.should have_selector "#enterprise_fee_set_collection_attributes_0_enterprise_id" - page.should have_selector "option[selected]", text: 'Packing' + page.should have_select "enterprise_fee_set_collection_attributes_0_enterprise_id" + page.should have_select "enterprise_fee_set_collection_attributes_0_fee_type", selected: '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_select "enterprise_fee_set_collection_attributes_0_tax_category_id", selected: 'GST' + page.should have_select "enterprise_fee_set_collection_attributes_0_calculator_type", selected: 'Flat Rate (per item)' page.should have_selector "input[value='#{amount}']" end @@ -57,7 +57,7 @@ feature %q{ scenario "editing an enterprise fee" do # Given an enterprise fee fee = create(:enterprise_fee) - create(:enterprise, name: 'Foo') + enterprise = create(:enterprise, name: 'Foo') # When I go to the enterprise fees page login_to_admin_section @@ -68,16 +68,26 @@ 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 'Inherit From Product', 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' # Then I should see the updated fields for my fee - page.should have_selector "option[selected]", text: 'Foo' - page.should have_selector "option[selected]", text: 'Admin' + page.should have_select "enterprise_fee_set_collection_attributes_0_enterprise_id", selected: 'Foo' + page.should have_select "enterprise_fee_set_collection_attributes_0_fee_type", selected: '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_select 'enterprise_fee_set_collection_attributes_0_tax_category_id', selected: 'Inherit From Product' page.should have_selector "option[selected]", text: 'Flat Percent' + + fee.reload + fee.enterprise.should == enterprise + fee.name.should == 'Greetings!' + fee.fee_type.should == 'admin' + fee.calculator_type.should == "Spree::Calculator::FlatPercentItemTotal" + + # Sets tax_category and inherits_tax_category + fee.tax_category.should == nil + fee.inherits_tax_category.should == true end scenario "deleting an enterprise fee" do diff --git a/spec/features/admin/enterprise_relationships_spec.rb b/spec/features/admin/enterprise_relationships_spec.rb index ef0f1e5537..85a0a87ee2 100644 --- a/spec/features/admin/enterprise_relationships_spec.rb +++ b/spec/features/admin/enterprise_relationships_spec.rb @@ -37,17 +37,17 @@ feature %q{ e2 = create(:enterprise, name: 'Two') visit admin_enterprise_relationships_path - select 'One', from: 'enterprise_relationship_parent_id' + select2_select 'One', from: 'enterprise_relationship_parent_id' check 'to add to order cycle' check 'to manage products' uncheck 'to manage products' check 'to edit profile' - check 'to override variant details' - select 'Two', from: 'enterprise_relationship_child_id' + check 'to add products to inventory' + select2_select 'Two', from: 'enterprise_relationship_child_id' click_button 'Create' - page.should have_relationship e1, e2, ['to add to order cycle', 'to override variant details', 'to edit profile'] + page.should have_relationship e1, e2, ['to add to order cycle', 'to add products to inventory', 'to edit profile'] er = EnterpriseRelationship.where(parent_id: e1, child_id: e2).first er.should be_present er.permissions.map(&:name).should match_array ['add_to_order_cycle', 'edit_profile', 'create_variant_overrides'] @@ -62,8 +62,8 @@ feature %q{ expect do # When I attempt to create a duplicate relationship visit admin_enterprise_relationships_path - select 'One', from: 'enterprise_relationship_parent_id' - select 'Two', from: 'enterprise_relationship_child_id' + select2_select 'One', from: 'enterprise_relationship_parent_id' + select2_select 'Two', from: 'enterprise_relationship_child_id' click_button 'Create' # Then I should see an error message @@ -110,8 +110,8 @@ feature %q{ scenario "enterprise user can only add their own enterprises as parent" do visit admin_enterprise_relationships_path - page.should have_select 'enterprise_relationship_parent_id', options: ['', d1.name] - page.should have_select 'enterprise_relationship_child_id', options: ['', d1.name, d2.name, d3.name] + page.should have_select2 'enterprise_relationship_parent_id', options: ['', d1.name] + page.should have_select2 'enterprise_relationship_child_id', options: ['', d1.name, d2.name, d3.name] end end diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index 378afa3626..fe74d9eb2d 100644 --- a/spec/features/admin/variant_overrides_spec.rb +++ b/spec/features/admin/variant_overrides_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' feature %q{ As an Administrator - With products I can add to order cycles + With products I can add to my hub's inventory I want to override the stock level and price of those products Without affecting other hubs that share the same products }, js: true do @@ -11,34 +11,43 @@ 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]) } + let!(:producer_managed) { create(:supplier_enterprise) } + let!(:producer_related) { create(:supplier_enterprise) } + let!(:producer_unrelated) { create(:supplier_enterprise) } + let!(:er1) { create(:enterprise_relationship, parent: producer, child: hub, + permissions_list: [:create_variant_overrides]) } + let!(:er2) { create(:enterprise_relationship, parent: producer_related, child: hub, + permissions_list: [:create_variant_overrides]) } context "as an enterprise user" do - let(:user) { create_enterprise_user enterprises: [hub2, producer] } + let(:user) { create_enterprise_user enterprises: [hub, producer_managed] } before { quick_login_as user } describe "selecting a hub" do - it "displays a list of hub choices" do - visit '/admin/variant_overrides' + let!(:er1) { create(:enterprise_relationship, parent: hub2, child: producer_managed, + permissions_list: [:add_to_order_cycle]) } # This er should not confer ability to create VOs for hub2 - page.should have_select2 'hub_id', options: ['', hub.name, hub2.name] + it "displays a list of hub choices (ie. only those managed by the user)" do + visit '/admin/inventory' + + page.should have_select2 'hub_id', options: [hub.name] # Selects the hub automatically when only one is available end end - context "when a hub is selected" do + context "when inventory_items exist for variants" do let!(:product) { create(:simple_product, supplier: producer, variant_unit: 'weight', variant_unit_scale: 1) } let!(:variant) { create(:variant, product: product, unit_value: 1, price: 1.23, on_hand: 12) } + let!(:inventory_item) { create(:inventory_item, enterprise: hub, variant: variant ) } + + let!(:product_managed) { create(:simple_product, supplier: producer_managed, variant_unit: 'weight', variant_unit_scale: 1) } + let!(:variant_managed) { create(:variant, product: product_managed, unit_value: 3, price: 3.65, on_hand: 2) } + let!(:inventory_item_managed) { create(:inventory_item, enterprise: hub, variant: variant_managed ) } - let!(:producer_related) { create(:supplier_enterprise) } let!(:product_related) { create(:simple_product, supplier: producer_related) } let!(:variant_related) { create(:variant, product: product_related, unit_value: 2, price: 2.34, on_hand: 23) } - let!(:er2) { create(:enterprise_relationship, parent: producer_related, child: hub, - permissions_list: [:create_variant_overrides]) } + let!(:inventory_item_related) { create(:inventory_item, enterprise: hub, variant: variant_related ) } - let!(:producer_unrelated) { create(:supplier_enterprise) } let!(:product_unrelated) { create(:simple_product, supplier: producer_unrelated) } @@ -47,85 +56,88 @@ feature %q{ variant.option_values.first.destroy end - context "with no overrides" do + context "when a hub is selected" do before do - visit '/admin/variant_overrides' + visit '/admin/inventory' select2_select hub.name, from: 'hub_id' end - it "displays the list of products with variants" do - page.should have_table_row ['PRODUCER', 'PRODUCT', 'PRICE', 'ON HAND'] - page.should have_table_row [producer.name, product.name, '', ''] - page.should have_input "variant-overrides-#{variant.id}-price", placeholder: '1.23' - page.should have_input "variant-overrides-#{variant.id}-count_on_hand", placeholder: '12' + context "with no overrides" do + it "displays the list of products with variants" do + page.should have_table_row ['PRODUCER', 'PRODUCT', 'PRICE', 'ON HAND'] + page.should have_table_row [producer.name, product.name, '', ''] + page.should have_input "variant-overrides-#{variant.id}-price", placeholder: '1.23' + page.should have_input "variant-overrides-#{variant.id}-count_on_hand", placeholder: '12' - page.should have_table_row [producer_related.name, product_related.name, '', ''] - page.should have_input "variant-overrides-#{variant_related.id}-price", placeholder: '2.34' - page.should have_input "variant-overrides-#{variant_related.id}-count_on_hand", placeholder: '23' + page.should have_table_row [producer_related.name, product_related.name, '', ''] + page.should have_input "variant-overrides-#{variant_related.id}-price", placeholder: '2.34' + page.should have_input "variant-overrides-#{variant_related.id}-count_on_hand", placeholder: '23' - # filters the products to those the hub can override - page.should_not have_content producer_unrelated.name - page.should_not have_content product_unrelated.name + # filters the products to those the hub can override + page.should_not have_content producer_managed.name + page.should_not have_content product_managed.name + page.should_not have_content producer_unrelated.name + page.should_not have_content product_unrelated.name - # Filters based on the producer select filter - expect(page).to have_selector "#v_#{variant.id}" - expect(page).to have_selector "#v_#{variant_related.id}" - select2_select producer.name, from: 'producer_filter' - expect(page).to have_selector "#v_#{variant.id}" - expect(page).to_not have_selector "#v_#{variant_related.id}" - select2_select 'All', from: 'producer_filter' + # Filters based on the producer select filter + expect(page).to have_selector "#v_#{variant.id}" + expect(page).to have_selector "#v_#{variant_related.id}" + select2_select producer.name, from: 'producer_filter' + expect(page).to have_selector "#v_#{variant.id}" + expect(page).to_not have_selector "#v_#{variant_related.id}" + select2_select 'All', from: 'producer_filter' - # Filters based on the quick search box - expect(page).to have_selector "#v_#{variant.id}" - expect(page).to have_selector "#v_#{variant_related.id}" - fill_in 'query', with: product.name - expect(page).to have_selector "#v_#{variant.id}" - expect(page).to_not have_selector "#v_#{variant_related.id}" - fill_in 'query', with: '' + # Filters based on the quick search box + expect(page).to have_selector "#v_#{variant.id}" + expect(page).to have_selector "#v_#{variant_related.id}" + fill_in 'query', with: product.name + expect(page).to have_selector "#v_#{variant.id}" + expect(page).to_not have_selector "#v_#{variant_related.id}" + fill_in 'query', with: '' - # Clears the filters - expect(page).to have_selector "#v_#{variant.id}" - expect(page).to have_selector "#v_#{variant_related.id}" - select2_select producer.name, from: 'producer_filter' - fill_in 'query', with: product_related.name - expect(page).to_not have_selector "#v_#{variant.id}" - expect(page).to_not have_selector "#v_#{variant_related.id}" - click_button 'Clear All' - expect(page).to have_selector "#v_#{variant.id}" - expect(page).to have_selector "#v_#{variant_related.id}" - end + # Clears the filters + expect(page).to have_selector "tr#v_#{variant.id}" + expect(page).to have_selector "tr#v_#{variant_related.id}" + select2_select producer.name, from: 'producer_filter' + fill_in 'query', with: product_related.name + expect(page).to_not have_selector "tr#v_#{variant.id}" + expect(page).to_not have_selector "tr#v_#{variant_related.id}" + click_button 'Clear All' + expect(page).to have_selector "tr#v_#{variant.id}" + expect(page).to have_selector "tr#v_#{variant_related.id}" - it "creates new overrides" do - first("div#columns-dropdown", :text => "COLUMNS").click - first("div#columns-dropdown div.menu div.menu_item", text: "SKU").click - first("div#columns-dropdown div.menu div.menu_item", text: "On Demand").click - first("div#columns-dropdown", :text => "COLUMNS").click + # Show/Hide products + first("div#columns-dropdown", :text => "COLUMNS").click + first("div#columns-dropdown div.menu div.menu_item", text: "Hide").click + first("div#columns-dropdown", :text => "COLUMNS").click + expect(page).to have_selector "tr#v_#{variant.id}" + expect(page).to have_selector "tr#v_#{variant_related.id}" + within "tr#v_#{variant.id}" do click_button 'Hide' end + expect(page).to_not have_selector "tr#v_#{variant.id}" + expect(page).to have_selector "tr#v_#{variant_related.id}" + first("div#views-dropdown").click + first("div#views-dropdown div.menu div.menu_item", text: "Hidden Products").click + expect(page).to have_selector "tr#v_#{variant.id}" + expect(page).to_not have_selector "tr#v_#{variant_related.id}" + within "tr#v_#{variant.id}" do click_button 'Add' end + expect(page).to_not have_selector "tr#v_#{variant.id}" + expect(page).to_not have_selector "tr#v_#{variant_related.id}" + first("div#views-dropdown").click + first("div#views-dropdown div.menu div.menu_item", text: "Inventory Products").click + expect(page).to have_selector "tr#v_#{variant.id}" + expect(page).to have_selector "tr#v_#{variant_related.id}" + end - fill_in "variant-overrides-#{variant.id}-sku", with: 'NEWSKU' - fill_in "variant-overrides-#{variant.id}-price", with: '777.77' - fill_in "variant-overrides-#{variant.id}-count_on_hand", with: '123' - check "variant-overrides-#{variant.id}-on_demand" - page.should have_content "Changes to one override remain unsaved." + it "creates new overrides" do + first("div#columns-dropdown", :text => "COLUMNS").click + first("div#columns-dropdown div.menu div.menu_item", text: "SKU").click + first("div#columns-dropdown div.menu div.menu_item", text: "On Demand").click + first("div#columns-dropdown", :text => "COLUMNS").click - expect do - click_button 'Save Changes' - page.should have_content "Changes saved." - end.to change(VariantOverride, :count).by(1) - - vo = VariantOverride.last - vo.variant_id.should == variant.id - vo.hub_id.should == hub.id - vo.sku.should == "NEWSKU" - vo.price.should == 777.77 - vo.count_on_hand.should == 123 - vo.on_demand.should == true - end - - describe "creating and then updating the new override" do - it "updates the same override instead of creating a duplicate" do - # When I create a new override + fill_in "variant-overrides-#{variant.id}-sku", with: 'NEWSKU' fill_in "variant-overrides-#{variant.id}-price", with: '777.77' fill_in "variant-overrides-#{variant.id}-count_on_hand", with: '123' + check "variant-overrides-#{variant.id}-on_demand" page.should have_content "Changes to one override remain unsaved." expect do @@ -133,137 +145,203 @@ feature %q{ page.should have_content "Changes saved." end.to change(VariantOverride, :count).by(1) - # And I update its settings without reloading the page - fill_in "variant-overrides-#{variant.id}-price", with: '111.11' - fill_in "variant-overrides-#{variant.id}-count_on_hand", with: '111' + vo = VariantOverride.last + vo.variant_id.should == variant.id + vo.hub_id.should == hub.id + vo.sku.should == "NEWSKU" + vo.price.should == 777.77 + vo.count_on_hand.should == 123 + vo.on_demand.should == true + end + + describe "creating and then updating the new override" do + it "updates the same override instead of creating a duplicate" do + # When I create a new override + fill_in "variant-overrides-#{variant.id}-price", with: '777.77' + fill_in "variant-overrides-#{variant.id}-count_on_hand", with: '123' + page.should have_content "Changes to one override remain unsaved." + + expect do + click_button 'Save Changes' + page.should have_content "Changes saved." + end.to change(VariantOverride, :count).by(1) + + # And I update its settings without reloading the page + fill_in "variant-overrides-#{variant.id}-price", with: '111.11' + fill_in "variant-overrides-#{variant.id}-count_on_hand", with: '111' + page.should have_content "Changes to one override remain unsaved." + + # Then I shouldn't see a new override + expect do + click_button 'Save Changes' + page.should have_content "Changes saved." + end.to change(VariantOverride, :count).by(0) + + # And the override should be updated + vo = VariantOverride.last + vo.variant_id.should == variant.id + vo.hub_id.should == hub.id + vo.price.should == 111.11 + vo.count_on_hand.should == 111 + end + end + + it "displays an error when unauthorised to access the page" do + fill_in "variant-overrides-#{variant.id}-price", with: '777.77' + fill_in "variant-overrides-#{variant.id}-count_on_hand", with: '123' + page.should have_content "Changes to one override remain unsaved." + + user.enterprises.clear + + expect do + click_button 'Save Changes' + page.should have_content "I couldn't get authorisation to save those changes, so they remain unsaved." + end.to change(VariantOverride, :count).by(0) + end + + it "displays an error when unauthorised to update a particular override" do + fill_in "variant-overrides-#{variant_related.id}-price", with: '777.77' + fill_in "variant-overrides-#{variant_related.id}-count_on_hand", with: '123' + page.should have_content "Changes to one override remain unsaved." + + er2.destroy + + expect do + click_button 'Save Changes' + page.should have_content "I couldn't get authorisation to save those changes, so they remain unsaved." + end.to change(VariantOverride, :count).by(0) + end + end + + context "with overrides" do + let!(:vo) { create(:variant_override, variant: variant, hub: hub, price: 77.77, count_on_hand: 11111, default_stock: 1000, resettable: true) } + let!(:vo_no_auth) { create(:variant_override, variant: variant, hub: hub2, price: 1, count_on_hand: 2) } + let!(:product2) { create(:simple_product, supplier: producer, variant_unit: 'weight', variant_unit_scale: 1) } + let!(:variant2) { create(:variant, product: product2, unit_value: 8, price: 1.00, on_hand: 12) } + let!(:inventory_item2) { create(:inventory_item, enterprise: hub, variant: variant2) } + let!(:vo_no_reset) { create(:variant_override, variant: variant2, hub: hub, price: 3.99, count_on_hand: 40, default_stock: 100, resettable: false) } + let!(:variant3) { create(:variant, product: product, unit_value: 2, price: 5.00, on_hand: 6) } + let!(:vo3) { create(:variant_override, variant: variant3, hub: hub, price: 6, count_on_hand: 7, sku: "SOMESKU", default_stock: 100, resettable: false) } + let!(:inventory_item3) { create(:inventory_item, enterprise: hub, variant: variant3) } + + before do + visit '/admin/inventory' + select2_select hub.name, from: 'hub_id' + end + + it "product values are affected by overrides" do + page.should have_input "variant-overrides-#{variant.id}-price", with: '77.77', placeholder: '1.23' + page.should have_input "variant-overrides-#{variant.id}-count_on_hand", with: '11111', placeholder: '12' + end + + it "updates existing overrides" do + fill_in "variant-overrides-#{variant.id}-price", with: '22.22' + fill_in "variant-overrides-#{variant.id}-count_on_hand", with: '8888' page.should have_content "Changes to one override remain unsaved." - # Then I shouldn't see a new override expect do click_button 'Save Changes' page.should have_content "Changes saved." end.to change(VariantOverride, :count).by(0) - # And the override should be updated - vo = VariantOverride.last + vo.reload vo.variant_id.should == variant.id vo.hub_id.should == hub.id - vo.price.should == 111.11 - vo.count_on_hand.should == 111 + vo.price.should == 22.22 + vo.count_on_hand.should == 8888 + end + + # Any new fields added to the VO model need to be added to this test + it "deletes overrides when values are cleared" do + first("div#columns-dropdown", :text => "COLUMNS").click + first("div#columns-dropdown div.menu div.menu_item", text: "On Demand").click + first("div#columns-dropdown div.menu div.menu_item", text: "Reset Stock Level").click + first("div#columns-dropdown", :text => "COLUMNS").click + + # Clearing values by 'inheriting' + first("div#columns-dropdown", :text => "COLUMNS").click + first("div#columns-dropdown div.menu div.menu_item", text: "Inheritance").click + first("div#columns-dropdown", :text => "COLUMNS").click + check "variant-overrides-#{variant3.id}-inherit" + + # Clearing values manually + fill_in "variant-overrides-#{variant.id}-price", with: '' + fill_in "variant-overrides-#{variant.id}-count_on_hand", with: '' + fill_in "variant-overrides-#{variant.id}-default_stock", with: '' + page.uncheck "variant-overrides-#{variant.id}-resettable" + page.should have_content "Changes to 2 overrides remain unsaved." + + expect do + click_button 'Save Changes' + page.should have_content "Changes saved." + end.to change(VariantOverride, :count).by(-2) + + VariantOverride.where(id: vo.id).should be_empty + VariantOverride.where(id: vo3.id).should be_empty + end + + it "resets stock to defaults" do + first("div#bulk-actions-dropdown").click + first("div#bulk-actions-dropdown div.menu div.menu_item", text: "Reset Stock Levels To Defaults").click + page.should have_content 'Stocks reset to defaults.' + vo.reload + page.should have_input "variant-overrides-#{variant.id}-count_on_hand", with: '1000', placeholder: '12' + vo.count_on_hand.should == 1000 + end + + it "doesn't reset stock levels if the behaviour is disabled" do + first("div#bulk-actions-dropdown").click + first("div#bulk-actions-dropdown div.menu div.menu_item", text: "Reset Stock Levels To Defaults").click + vo_no_reset.reload + page.should have_input "variant-overrides-#{variant2.id}-count_on_hand", with: '40', placeholder: '12' + vo_no_reset.count_on_hand.should == 40 + end + + it "prompts to save changes before reset if any are pending" do + fill_in "variant-overrides-#{variant.id}-price", with: '200' + first("div#bulk-actions-dropdown").click + first("div#bulk-actions-dropdown div.menu div.menu_item", text: "Reset Stock Levels To Defaults").click + page.should have_content "Save changes first" end end - - it "displays an error when unauthorised to access the page" do - fill_in "variant-overrides-#{variant.id}-price", with: '777.77' - fill_in "variant-overrides-#{variant.id}-count_on_hand", with: '123' - page.should have_content "Changes to one override remain unsaved." - - user.enterprises.clear - - expect do - click_button 'Save Changes' - page.should have_content "I couldn't get authorisation to save those changes, so they remain unsaved." - end.to change(VariantOverride, :count).by(0) - end - - it "displays an error when unauthorised to update a particular override" do - fill_in "variant-overrides-#{variant_related.id}-price", with: '777.77' - fill_in "variant-overrides-#{variant_related.id}-count_on_hand", with: '123' - page.should have_content "Changes to one override remain unsaved." - - er2.destroy - - expect do - click_button 'Save Changes' - page.should have_content "I couldn't get authorisation to save those changes, so they remain unsaved." - end.to change(VariantOverride, :count).by(0) - end end + end - context "with overrides" do - let!(:vo) { create(:variant_override, variant: variant, hub: hub, price: 77.77, count_on_hand: 11111, default_stock: 1000, resettable: true) } - let!(:vo_no_auth) { create(:variant_override, variant: variant, hub: hub3, price: 1, count_on_hand: 2) } - let!(:product2) { create(:simple_product, supplier: producer, variant_unit: 'weight', variant_unit_scale: 1) } - let!(:variant2) { create(:variant, product: product2, unit_value: 8, price: 1.00, on_hand: 12) } - let!(:vo_no_reset) { create(:variant_override, variant: variant2, hub: hub, price: 3.99, count_on_hand: 40, default_stock: 100, resettable: false) } - let!(:variant3) { create(:variant, product: product, unit_value: 2, price: 5.00, on_hand: 6) } - let!(:vo3) { create(:variant_override, variant: variant3, hub: hub, price: 6, count_on_hand: 7, sku: "SOMESKU", default_stock: 100, resettable: false) } + describe "when inventory_items do not exist for variants" do + let!(:product) { create(:simple_product, supplier: producer, variant_unit: 'weight', variant_unit_scale: 1) } + let!(:variant1) { create(:variant, product: product, unit_value: 1, price: 1.23, on_hand: 12) } + let!(:variant2) { create(:variant, product: product, unit_value: 2, price: 4.56, on_hand: 3) } + context "when a hub is selected" do before do - visit '/admin/variant_overrides' + visit '/admin/inventory' select2_select hub.name, from: 'hub_id' end - it "product values are affected by overrides" do - page.should have_input "variant-overrides-#{variant.id}-price", with: '77.77', placeholder: '1.23' - page.should have_input "variant-overrides-#{variant.id}-count_on_hand", with: '11111', placeholder: '12' - end + it "alerts the user to the presence of new products, and allows them to be added or hidden" do + expect(page).to_not have_selector "table#variant-overrides tr#v_#{variant1.id}" + expect(page).to_not have_selector "table#variant-overrides tr#v_#{variant2.id}" - it "updates existing overrides" do - fill_in "variant-overrides-#{variant.id}-price", with: '22.22' - fill_in "variant-overrides-#{variant.id}-count_on_hand", with: '8888' - page.should have_content "Changes to one override remain unsaved." + expect(page).to have_selector '.alert-row span.message', text: "There are 1 new products available to add to your inventory." + click_button "Review Now" - expect do - click_button 'Save Changes' - page.should have_content "Changes saved." - end.to change(VariantOverride, :count).by(0) + expect(page).to have_table_row ['PRODUCER', 'PRODUCT', 'VARIANT', 'ADD', 'HIDE'] + expect(page).to have_selector "table#new-products tr#v_#{variant1.id}" + expect(page).to have_selector "table#new-products tr#v_#{variant2.id}" + within "table#new-products tr#v_#{variant1.id}" do click_button 'Add' end + within "table#new-products tr#v_#{variant2.id}" do click_button 'Hide' end + expect(page).to_not have_selector "table#new-products tr#v_#{variant1.id}" + expect(page).to_not have_selector "table#new-products tr#v_#{variant2.id}" + click_button "Back to my inventory" - vo.reload - vo.variant_id.should == variant.id - vo.hub_id.should == hub.id - vo.price.should == 22.22 - vo.count_on_hand.should == 8888 - end + expect(page).to have_selector "table#variant-overrides tr#v_#{variant1.id}" + expect(page).to_not have_selector "table#variant-overrides tr#v_#{variant2.id}" - # Any new fields added to the VO model need to be added to this test - it "deletes overrides when values are cleared" do - first("div#columns-dropdown", :text => "COLUMNS").click - first("div#columns-dropdown div.menu div.menu_item", text: "On Demand").click - first("div#columns-dropdown div.menu div.menu_item", text: "Reset Stock Level").click - first("div#columns-dropdown", :text => "COLUMNS").click + first("div#views-dropdown").click + first("div#views-dropdown div.menu div.menu_item", text: "Hidden Products").click - # Clearing values manually - fill_in "variant-overrides-#{variant.id}-price", with: '' - fill_in "variant-overrides-#{variant.id}-count_on_hand", with: '' - fill_in "variant-overrides-#{variant.id}-default_stock", with: '' - page.uncheck "variant-overrides-#{variant.id}-resettable" - page.should have_content "Changes to one override remain unsaved." - - # Clearing values by 'inheriting' - first("div#columns-dropdown", :text => "COLUMNS").click - first("div#columns-dropdown div.menu div.menu_item", text: "Inheritance").click - first("div#columns-dropdown", :text => "COLUMNS").click - page.check "variant-overrides-#{variant3.id}-inherit" - - expect do - click_button 'Save Changes' - page.should have_content "Changes saved." - end.to change(VariantOverride, :count).by(-2) - - VariantOverride.where(id: vo.id).should be_empty - VariantOverride.where(id: vo3.id).should be_empty - end - - it "resets stock to defaults" do - click_button 'Reset Stock to Defaults' - page.should have_content 'Stocks reset to defaults.' - vo.reload - page.should have_input "variant-overrides-#{variant.id}-count_on_hand", with: '1000', placeholder: '12' - vo.count_on_hand.should == 1000 - end - - it "doesn't reset stock levels if the behaviour is disabled" do - click_button 'Reset Stock to Defaults' - vo_no_reset.reload - page.should have_input "variant-overrides-#{variant2.id}-count_on_hand", with: '40', placeholder: '12' - vo_no_reset.count_on_hand.should == 40 - end - - it "prompts to save changes before reset if any are pending" do - fill_in "variant-overrides-#{variant.id}-price", with: '200' - click_button 'Reset Stock to Defaults' - page.should have_content "Save changes first" + expect(page).to_not have_selector "table#hidden-products tr#v_#{variant1.id}" + expect(page).to have_selector "table#hidden-products tr#v_#{variant2.id}" end end end diff --git a/spec/javascripts/unit/admin/controllers/variant_overrides_controller_spec.js.coffee b/spec/javascripts/unit/admin/controllers/variant_overrides_controller_spec.js.coffee index 699e1bc4ab..de3a3d5d55 100644 --- a/spec/javascripts/unit/admin/controllers/variant_overrides_controller_spec.js.coffee +++ b/spec/javascripts/unit/admin/controllers/variant_overrides_controller_spec.js.coffee @@ -9,6 +9,7 @@ describe "VariantOverridesCtrl", -> variantOverrides = {} DirtyVariantOverrides = null dirtyVariantOverrides = {} + inventoryItems = {} StatusMessage = null statusMessage = {} @@ -18,6 +19,7 @@ describe "VariantOverridesCtrl", -> $provide.value 'SpreeApiKey', 'API_KEY' $provide.value 'variantOverrides', variantOverrides $provide.value 'dirtyVariantOverrides', dirtyVariantOverrides + $provide.value 'inventoryItems', inventoryItems null inject ($controller, _VariantOverrides_, _DirtyVariantOverrides_, _StatusMessage_) -> @@ -26,9 +28,20 @@ describe "VariantOverridesCtrl", -> StatusMessage = _StatusMessage_ ctrl = $controller 'AdminVariantOverridesCtrl', { $scope: scope, hubs: hubs, producers: producers, products: products, hubPermissions: hubPermissions, VariantOverrides: VariantOverrides, DirtyVariantOverrides: DirtyVariantOverrides, StatusMessage: StatusMessage} - it "initialises the hub list and the chosen hub", -> - expect(scope.hubs).toEqual { 1: {id: 1, name: 'Hub'} } - expect(scope.hub).toBeNull() + describe "when only one hub is available", -> + it "initialises the hub list and the selects the only hub in the list", -> + expect(scope.hubs).toEqual { 1: {id: 1, name: 'Hub'} } + expect(scope.hub_id).toEqual 1 + + describe "when more than one hub is available", -> + beforeEach -> + inject ($controller) -> + hubs = [{id: 1, name: 'Hub1'}, {id: 12, name: 'Hub2'}] + $controller 'AdminVariantOverridesCtrl', { $scope: scope, hubs: hubs, producers: [], products: [], hubPermissions: []} + + it "initialises the hub list and the selects the only hub in the list", -> + expect(scope.hubs).toEqual { 1: {id: 1, name: 'Hub1'}, 12: {id: 12, name: 'Hub2'} } + expect(scope.hub_id).toBeNull() it "initialises select filters", -> expect(scope.producerFilter).toEqual 0 @@ -43,17 +56,6 @@ describe "VariantOverridesCtrl", -> expect(scope.products).toEqual ['a', 'b', 'c', 'd'] expect(VariantOverrides.ensureDataFor).toHaveBeenCalled() - describe "selecting a hub", -> - it "sets the chosen hub", -> - scope.hub_id = 1 - scope.selectHub() - expect(scope.hub).toEqual hubs[0] - - it "does nothing when no selection has been made", -> - scope.hub_id = '' - scope.selectHub - expect(scope.hub).toBeNull - describe "updating", -> describe "error messages", -> it "returns an unauthorised message upon 401", -> diff --git a/spec/javascripts/unit/admin/index_utils/services/views_spec.js.coffee b/spec/javascripts/unit/admin/index_utils/services/views_spec.js.coffee new file mode 100644 index 0000000000..7333882d5d --- /dev/null +++ b/spec/javascripts/unit/admin/index_utils/services/views_spec.js.coffee @@ -0,0 +1,37 @@ +describe "Views service", -> + Views = null + + beforeEach -> + module 'admin.indexUtils' + + inject (_Views_) -> + Views = _Views_ + + describe "setting views", -> + beforeEach -> + spyOn(Views, "selectView").andCallThrough() + Views.setViews + view1: { name: 'View1', visible: true } + view2: { name: 'View2', visible: false } + view3: { name: 'View3', visible: true } + + it "sets resets @views and copies each view of the provided object across", -> + expect(Object.keys(Views.views)).toEqual ['view1', 'view2', 'view3'] + + it "calls selectView if visible is true", -> + expect(Views.selectView).toHaveBeenCalledWith('view1') + expect(Views.selectView).not.toHaveBeenCalledWith('view2'); + expect(Views.selectView).toHaveBeenCalledWith('view3') + expect(view.visible for key, view of Views.views).toEqual [false, false, true] + + describe "selecting a view", -> + beforeEach -> + Views.currentView = "some View" + Views.views = { view7: { name: 'View7', visible: false } } + Views.selectView('view7') + + it "sets the currentView", -> + expect(Views.currentView.name).toEqual 'View7' + + it "switches the visibility of the given view", -> + expect(Views.currentView).toEqual { name: 'View7', visible: true } diff --git a/spec/javascripts/unit/admin/inventory_items/services/inventory_items_spec.js.coffee b/spec/javascripts/unit/admin/inventory_items/services/inventory_items_spec.js.coffee new file mode 100644 index 0000000000..49ea827900 --- /dev/null +++ b/spec/javascripts/unit/admin/inventory_items/services/inventory_items_spec.js.coffee @@ -0,0 +1,73 @@ +describe "InventoryItems service", -> + InventoryItems = InventoryItemResource = inventoryItems = $httpBackend = null + inventoryItems = {} + + beforeEach -> + module 'admin.inventoryItems' + module ($provide) -> + $provide.value 'inventoryItems', inventoryItems + null + + this.addMatchers + toDeepEqual: (expected) -> + return angular.equals(this.actual, expected) + + inject ($q, _$httpBackend_, _InventoryItems_, _InventoryItemResource_) -> + InventoryItems = _InventoryItems_ + InventoryItemResource = _InventoryItemResource_ + $httpBackend = _$httpBackend_ + + + describe "#setVisiblity", -> + describe "on an inventory item that already exists", -> + existing = null + + beforeEach -> + existing = new InventoryItemResource({ id: 1, enterprise_id: 2, variant_id: 3, visible: true }) + InventoryItems.inventoryItems[2] = {} + InventoryItems.inventoryItems[2][3] = existing + + describe "success", -> + beforeEach -> + $httpBackend.expectPUT('/admin/inventory_items/1.json', { id: 1, enterprise_id: 2, variant_id: 3, visible: false } ) + .respond 200, { id: 1, enterprise_id: 2, variant_id: 3, visible: false } + InventoryItems.setVisibility(2,3,false) + + it "saves the new visible value AFTER the request responds successfully", -> + expect(InventoryItems.inventoryItems[2][3].visible).toBe true + $httpBackend.flush() + expect(InventoryItems.inventoryItems[2][3].visible).toBe false + + describe "failure", -> + beforeEach -> + $httpBackend.expectPUT('/admin/inventory_items/1.json',{ id: 1, enterprise_id: 2, variant_id: 3, visible: null }) + .respond 422, { errors: ["Visible must be true or false"] } + InventoryItems.setVisibility(2,3,null) + + it "store the errors in the errors object", -> + expect(InventoryItems.errors).toEqual {} + $httpBackend.flush() + expect(InventoryItems.errors[2][3]).toEqual ["Visible must be true or false"] + + describe "on an inventory item that does not exist", -> + describe "success", -> + beforeEach -> + $httpBackend.expectPOST('/admin/inventory_items.json', { enterprise_id: 5, variant_id: 6, visible: false } ) + .respond 200, { id: 1, enterprise_id: 2, variant_id: 3, visible: false } + InventoryItems.setVisibility(5,6,false) + + it "saves the new visible value AFTER the request responds successfully", -> + expect(InventoryItems.inventoryItems).toEqual {} + $httpBackend.flush() + expect(InventoryItems.inventoryItems[5][6].visible).toBe false + + describe "failure", -> + beforeEach -> + $httpBackend.expectPOST('/admin/inventory_items.json',{ enterprise_id: 5, variant_id: 6, visible: null }) + .respond 422, { errors: ["Visible must be true or false"] } + InventoryItems.setVisibility(5,6,null) + + it "store the errors in the errors object", -> + expect(InventoryItems.errors).toEqual {} + $httpBackend.flush() + expect(InventoryItems.errors[5][6]).toEqual ["Visible must be true or false"] diff --git a/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee index 0d0a01215d..96ac4a8905 100644 --- a/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee +++ b/spec/javascripts/unit/admin/services/enterprise_relationships_spec.js.coffee @@ -15,4 +15,4 @@ describe "enterprise relationships", -> expect(EnterpriseRelationships.permission_presentation("add_to_order_cycle")).toEqual "add to order cycle" expect(EnterpriseRelationships.permission_presentation("manage_products")).toEqual "manage products" expect(EnterpriseRelationships.permission_presentation("edit_profile")).toEqual "edit profile" - expect(EnterpriseRelationships.permission_presentation("create_variant_overrides")).toEqual "override variant details" + expect(EnterpriseRelationships.permission_presentation("create_variant_overrides")).toEqual "add products to inventory" diff --git a/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb index d2d673e5ab..a31295ac9d 100644 --- a/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb +++ b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb @@ -194,77 +194,90 @@ module OpenFoodNetwork end end - describe "creating adjustments for a line item" do - let(:oc) { OrderCycle.new } - let(:variant) { double(:variant) } - let(:distributor) { double(:distributor) } - let(:order) { double(:order, distributor: distributor, order_cycle: oc) } - let(:line_item) { double(:line_item, variant: variant, order: order) } - - it "creates an adjustment for each fee" do - applicator = double(:enterprise_fee_applicator) - applicator.should_receive(:create_line_item_adjustment).with(line_item) - - efc = EnterpriseFeeCalculator.new - efc.should_receive(:per_item_enterprise_fee_applicators_for).with(variant) { [applicator] } - - efc.create_line_item_adjustments_for line_item - end - - it "makes fee applicators for a line item" do - distributor = double(:distributor) - ef1 = double(:enterprise_fee) - ef2 = double(:enterprise_fee) - ef3 = double(:enterprise_fee) - incoming_exchange = double(:exchange, role: 'supplier') - outgoing_exchange = double(:exchange, role: 'distributor') - incoming_exchange.stub_chain(:enterprise_fees, :per_item) { [ef1] } - outgoing_exchange.stub_chain(:enterprise_fees, :per_item) { [ef2] } - - oc.stub(:exchanges_carrying) { [incoming_exchange, outgoing_exchange] } - oc.stub_chain(:coordinator_fees, :per_item) { [ef3] } - - efc = EnterpriseFeeCalculator.new(distributor, oc) - efc.send(:per_item_enterprise_fee_applicators_for, line_item.variant).should == - [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, line_item.variant, 'supplier'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, line_item.variant, 'distributor'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, line_item.variant, 'coordinator')] - end - end - - describe "creating adjustments for an order" do + describe "creating adjustments" do let(:oc) { OrderCycle.new } let(:distributor) { double(:distributor) } - let(:order) { double(:order, distributor: distributor, order_cycle: oc) } + let(:ef1) { double(:enterprise_fee) } + let(:ef2) { double(:enterprise_fee) } + let(:ef3) { double(:enterprise_fee) } + let(:incoming_exchange) { double(:exchange, role: 'supplier') } + let(:outgoing_exchange) { double(:exchange, role: 'distributor') } + let(:applicator) { double(:enterprise_fee_applicator) } - it "creates an adjustment for each fee" do - applicator = double(:enterprise_fee_applicator) - applicator.should_receive(:create_order_adjustment).with(order) - efc = EnterpriseFeeCalculator.new - efc.should_receive(:per_order_enterprise_fee_applicators_for).with(order) { [applicator] } + describe "for a line item" do + let(:variant) { double(:variant) } + let(:line_item) { double(:line_item, variant: variant, order: order) } - efc.create_order_adjustments_for order + before do + allow(incoming_exchange).to receive(:enterprise_fees) { double(:enterprise_fees, per_item: [ef1]) } + allow(outgoing_exchange).to receive(:enterprise_fees) { double(:enterprise_fees, per_item: [ef2]) } + allow(oc).to receive(:exchanges_carrying) { [incoming_exchange, outgoing_exchange] } + allow(oc).to receive(:coordinator_fees) { double(:coodinator_fees, per_item: [ef3]) } + end + + context "with order_cycle and distributor set" do + let(:efc) { EnterpriseFeeCalculator.new(distributor, oc) } + let(:order) { double(:order, distributor: distributor, order_cycle: oc) } + + it "creates an adjustment for each fee" do + expect(efc).to receive(:per_item_enterprise_fee_applicators_for).with(variant) { [applicator] } + expect(applicator).to receive(:create_line_item_adjustment).with(line_item) + efc.create_line_item_adjustments_for line_item + end + + it "makes fee applicators for a line item" do + expect(efc.send(:per_item_enterprise_fee_applicators_for, line_item.variant)) + .to eq [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, line_item.variant, 'supplier'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, line_item.variant, 'distributor'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, line_item.variant, 'coordinator')] + end + end + + context "with no order_cycle or distributor set" do + let(:efc) { EnterpriseFeeCalculator.new } + let(:order) { double(:order, distributor: nil, order_cycle: nil) } + + it "does not make applicators for an order" do + expect(efc.send(:per_item_enterprise_fee_applicators_for, line_item.variant)).to eq [] + end + end end - it "makes fee applicators for an order" do - distributor = double(:distributor) - ef1 = double(:enterprise_fee) - ef2 = double(:enterprise_fee) - ef3 = double(:enterprise_fee) - incoming_exchange = double(:exchange, role: 'supplier') - outgoing_exchange = double(:exchange, role: 'distributor') - incoming_exchange.stub_chain(:enterprise_fees, :per_order) { [ef1] } - outgoing_exchange.stub_chain(:enterprise_fees, :per_order) { [ef2] } + describe "for an order" do + before do + allow(incoming_exchange).to receive(:enterprise_fees) { double(:enterprise_fees, per_order: [ef1]) } + allow(outgoing_exchange).to receive(:enterprise_fees) { double(:enterprise_fees, per_order: [ef2]) } + allow(oc).to receive(:exchanges_supplying) { [incoming_exchange, outgoing_exchange] } + allow(oc).to receive(:coordinator_fees) { double(:coodinator_fees, per_order: [ef3]) } + end - oc.stub(:exchanges_supplying) { [incoming_exchange, outgoing_exchange] } - oc.stub_chain(:coordinator_fees, :per_order) { [ef3] } + context "with order_cycle and distributor set" do + let(:efc) { EnterpriseFeeCalculator.new(distributor, oc) } + let(:order) { double(:order, distributor: distributor, order_cycle: oc) } - efc = EnterpriseFeeCalculator.new(distributor, oc) - efc.send(:per_order_enterprise_fee_applicators_for, order).should == - [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, nil, 'supplier'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, nil, 'distributor'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, nil, 'coordinator')] + it "creates an adjustment for each fee" do + expect(efc).to receive(:per_order_enterprise_fee_applicators_for).with(order) { [applicator] } + expect(applicator).to receive(:create_order_adjustment).with(order) + efc.create_order_adjustments_for order + end + + it "makes fee applicators for an order" do + expect(efc.send(:per_order_enterprise_fee_applicators_for, order)) + .to eq [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, nil, 'supplier'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, nil, 'distributor'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, nil, 'coordinator')] + end + end + + context "with no order_cycle or distributor set" do + let(:efc) { EnterpriseFeeCalculator.new } + let(:order) { double(:order, distributor: nil, order_cycle: nil) } + + it "does not make applicators for an order" do + expect(efc.send(:per_order_enterprise_fee_applicators_for, order)).to eq [] + end + end 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 6cbe6693aa..89dcb364b8 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 @@ -160,12 +160,27 @@ module OpenFoodNetwork context "when an exchange is passed in" do let(:v1) { create(:variant) } - let(:exchange) { create(:exchange, variants: [v1]) } + let(:v2) { create(:variant) } + let(:v3) { create(:variant) } + let(:exchange) { create(:exchange, variants: [v1, v2, v3]) } 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 + before do + allow(applicator).to receive(:editable_variant_ids_for_outgoing_exchange_between) { [ v1.id, v2.id ] } + end + + it "returns a hash with variant ids as keys" do + expect(hash.length).to be 3 + expect(hash.keys).to include v1.id, v2.id, v3.id + end + + it "editable variant ids are set to false" do + expect(hash[v1.id]).to be false + expect(hash[v2.id]).to be false + end + + it "and non-editable variant ids are set to true" do + expect(hash[v3.id]).to be true end end end @@ -209,7 +224,7 @@ module OpenFoodNetwork before do applicator.stub(:find_outgoing_exchange) { exchange_mock } applicator.stub(:incoming_variant_ids) { [1, 2, 3, 4] } - expect(applicator).to receive(:editable_variant_ids_for_outgoing_exchange_between). + allow(applicator).to receive(:editable_variant_ids_for_outgoing_exchange_between). with(coordinator_mock, enterprise_mock) { [1, 2, 3] } end @@ -234,6 +249,16 @@ module OpenFoodNetwork expect(ids).to eq [1, 3] end + it "removes variants which the user has permission to remove and that are not included in the submitted data" do + allow(exchange_mock).to receive(:incoming?) { false } + allow(exchange_mock).to receive(:variants) { [double(:variant, id: 1), double(:variant, id: 2), double(:variant, id: 3)] } + allow(exchange_mock).to receive(:sender) { coordinator_mock } + allow(exchange_mock).to receive(:receiver) { enterprise_mock } + applicator.stub(:incoming_variant_ids) { [1, 2, 3] } + ids = applicator.send(:outgoing_exchange_variant_ids, {:enterprise_id => 123, :variants => {'1' => true, '3' => true}}) + expect(ids).to eq [1, 3] + end + it "removes variants which are not included in incoming exchanges" do applicator.stub(:incoming_variant_ids) { [1, 2] } applicator.stub(:persisted_variants_hash) { {3 => true} } diff --git a/spec/lib/open_food_network/permissions_spec.rb b/spec/lib/open_food_network/permissions_spec.rb index a91db5f579..a26db2aa98 100644 --- a/spec/lib/open_food_network/permissions_spec.rb +++ b/spec/lib/open_food_network/permissions_spec.rb @@ -119,7 +119,7 @@ module OpenFoodNetwork {hub.id => [producer.id]} end - it "returns only permissions relating to managed enterprises" do + it "returns only permissions relating to managed hubs" do create(:enterprise_relationship, parent: e1, child: e2, permissions_list: [:create_variant_overrides]) @@ -137,31 +137,30 @@ module OpenFoodNetwork end describe "hubs connected to the user by relationships only" do - # producer_managed can add hub to order cycle - # hub can create variant overrides for producer - # we manage producer_managed - # therefore, we should be able to create variant overrides for hub on producer's products - let!(:producer_managed) { create(:supplier_enterprise) } let!(:er_oc) { create(:enterprise_relationship, parent: hub, child: producer_managed, - permissions_list: [:add_to_order_cycle]) } + permissions_list: [:add_to_order_cycle, :create_variant_overrides]) } before do permissions.stub(:managed_enterprises) { Enterprise.where(id: producer_managed.id) } end - it "allows the hub to create variant overrides for the producer" do - permissions.variant_override_enterprises_per_hub.should == - {hub.id => [producer.id, producer_managed.id]} + it "does not allow the user to create variant overrides for the hub" do + permissions.variant_override_enterprises_per_hub.should == {} end end - it "also returns managed producers" do + it "does not return managed producers (ie. only uses explicitly granted VO permissions)" do producer2 = create(:supplier_enterprise) permissions.stub(:managed_enterprises) { Enterprise.where(id: [hub, producer2]) } - permissions.variant_override_enterprises_per_hub.should == - {hub.id => [producer.id, producer2.id]} + expect(permissions.variant_override_enterprises_per_hub[hub.id]).to_not include producer2.id + end + + it "returns itself if self is also a primary producer (even when no explicit permission exists)" do + hub.update_attribute(:is_primary_producer, true) + + expect(permissions.variant_override_enterprises_per_hub[hub.id]).to include hub.id end end diff --git a/spec/lib/open_food_network/products_renderer_spec.rb b/spec/lib/open_food_network/products_renderer_spec.rb new file mode 100644 index 0000000000..a231fd3688 --- /dev/null +++ b/spec/lib/open_food_network/products_renderer_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper' +require 'open_food_network/products_renderer' + +module OpenFoodNetwork + describe ProductsRenderer do + let(:d) { create(:distributor_enterprise) } + let(:order_cycle) { create(:simple_order_cycle, distributors: [d], coordinator: create(:distributor_enterprise)) } + let(:exchange) { Exchange.find(order_cycle.exchanges.to_enterprises(d).outgoing.first.id) } + let(:pr) { ProductsRenderer.new(d, order_cycle) } + + describe "sorting" do + let(:t1) { create(:taxon) } + let(:t2) { create(:taxon) } + let!(:p1) { create(:product, name: "abc", primary_taxon_id: t2.id) } + let!(:p2) { create(:product, name: "def", primary_taxon_id: t1.id) } + let!(:p3) { create(:product, name: "ghi", primary_taxon_id: t2.id) } + let!(:p4) { create(:product, name: "jkl", primary_taxon_id: t1.id) } + + before do + exchange.variants << p1.variants.first + exchange.variants << p2.variants.first + exchange.variants << p3.variants.first + exchange.variants << p4.variants.first + end + + it "sorts products by the distributor's preferred taxon list" do + d.stub(:preferred_shopfront_taxon_order) {"#{t1.id},#{t2.id}"} + products = pr.send(:products_for_shop) + products.should == [p2, p4, p1, p3] + end + + it "alphabetizes products by name when taxon list is not set" do + d.stub(:preferred_shopfront_taxon_order) {""} + products = pr.send(:products_for_shop) + products.should == [p1, p2, p3, p4] + end + end + + context "JSON tests" do + let(:product) { create(:product) } + let(:variant) { product.variants.first } + + before do + exchange.variants << variant + end + + it "only returns products for the current order cycle" do + pr.products.should include product.name + end + + it "doesn't return products not in stock" do + variant.update_attribute(:count_on_hand, 0) + pr.products.should_not include product.name + end + + it "strips html from description" do + product.update_attribute(:description, "turtles frogs") + json = pr.products + json.should include "frogs" + json.should_not include "