diff --git a/Gemfile.lock b/Gemfile.lock index 75763fb4a0..6dd9888f8c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -730,6 +730,3 @@ DEPENDENCIES whenever wicked_pdf wkhtmltopdf-binary - -BUNDLED WITH - 1.10.6 diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 60ca6f330a..344c5e8a5d 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -27,8 +27,10 @@ //= 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/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 79b1876263..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/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/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/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/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/darkswarm/services/enterprise_registration_service.js.coffee b/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee index 102da60f19..a7d6a657fa 100644 --- a/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee +++ b/app/assets/javascripts/darkswarm/services/enterprise_registration_service.js.coffee @@ -2,7 +2,7 @@ Darkswarm.factory "EnterpriseRegistrationService", ($http, RegistrationService, new class EnterpriseRegistrationService enterprise: user_ids: [CurrentUser.id] - email: CurrentUser.email + email_address: CurrentUser.email address: {} country: availableCountries[0] 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/javascripts/templates/partials/contact.html.haml b/app/assets/javascripts/templates/partials/contact.html.haml index bdde30b8bb..12aa5ff061 100644 --- a/app/assets/javascripts/templates/partials/contact.html.haml +++ b/app/assets/javascripts/templates/partials/contact.html.haml @@ -1,11 +1,11 @@ %div.contact-container{bindonce: true} - %div.modal-centered{"bo-if" => "enterprise.email || enterprise.website || enterprise.phone"} + %div.modal-centered{"bo-if" => "enterprise.email_address || enterprise.website || enterprise.phone"} %p.modal-header {{'contact' | t}} %p{"bo-if" => "enterprise.phone", "bo-text" => "enterprise.phone"} - %p.word-wrap{"ng-if" => "enterprise.email"} - %a{"bo-href" => "enterprise.email | stripUrl", target: "_blank", mailto: true} - %span.email{"bo-bind" => "enterprise.email | stripUrl"} + %p.word-wrap{"ng-if" => "enterprise.email_address"} + %a{"bo-href" => "enterprise.email_address | stripUrl", target: "_blank", mailto: true} + %span.email{"bo-bind" => "enterprise.email_address | stripUrl"} %p.word-wrap{"ng-if" => "enterprise.website"} %a{"bo-href-i" => "http://{{enterprise.website | stripUrl}}", target: "_blank", "bo-bind" => "enterprise.website | stripUrl"} diff --git a/app/assets/javascripts/templates/registration/contact.html.haml b/app/assets/javascripts/templates/registration/contact.html.haml index b2a2164910..a22b0677a7 100644 --- a/app/assets/javascripts/templates/registration/contact.html.haml +++ b/app/assets/javascripts/templates/registration/contact.html.haml @@ -17,28 +17,13 @@ {{'enterprise_contact_required' | t}} .row .small-12.columns.field - %label{ for: 'enterprise_email' } {{'enterprise_email' | t}}: - %input.chunky.small-12.columns{ id: 'enterprise_email', name: 'email', type: 'email', required: true, placeholder: "eg. charlie@thefarm.com", ng: { model: 'enterprise.email' } } - %span.error.small-12.columns{ ng: { show: "(contact.email.$error.email || contact.email.$error.required) && submitted" } } - {{'enterprise_email_required' | t}} + %label{ for: 'enterprise_email_address' } {{'enterprise_email_address' | t}}: + %input.chunky.small-12.columns{ id: 'enterprise_email_address', name: 'email_address', type: 'email', placeholder: "eg. charlie@thefarm.com", ng: { model: 'enterprise.email_address' } } .row .small-12.columns.field %label{ for: 'enterprise_phone' } {{'enterprise_phone' | t}}: %input.chunky.small-12.columns{ id: 'enterprise_phone', name: 'phone', placeholder: "eg. (03) 1234 5678", ng: { model: 'enterprise.phone' } } .small-12.medium-12.large-5.hide-for-small-only - / %h6 - / Contact display - / %i.ofn-i_013-help.has-tip{ 'data-tooltip' => true, title: "Choose how you want to display your contact details on the Open Food Network."} - / .row - / .small-12.columns - / %label.indent-checkbox - / %input{ type: 'checkbox', id: 'contact_name_profile', ng: { model: 'enterprise.name_in_profile' } }   Display name in profile - / .small-12.columns - / %label.indent-checkbox - / %input{ type: 'checkbox', id: 'contact_email_profile', ng: { model: 'enterprise.email_in_profile' } }   Display email in profile - / .small-12.columns - / %label.indent-checkbox - / %input{ type: 'checkbox', id: 'contact_phone_profile', ng: { model: 'enterprise.phone_in_profile' } }   Display phone in profile .row.buttons .small-12.columns 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/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/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/assets/stylesheets/darkswarm/modal-enterprises.css.sass b/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass index e0090ca163..e4724a13f5 100644 --- a/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass +++ b/app/assets/stylesheets/darkswarm/modal-enterprises.css.sass @@ -62,10 +62,8 @@ // ABOUT Enterprise .about-container - max-height: 200px min-height: 20px margin-bottom: 0.5rem - overflow-y: scroll overflow-x: hidden border-bottom: 1px solid $light-grey @include box-shadow(0 2px 2px -2px $light-grey) 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/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/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/models/enterprise.rb b/app/models/enterprise.rb index 58f47c5ed1..ebe4d6289a 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -7,6 +7,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 @@ -35,6 +40,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 @@ -74,6 +80,7 @@ class Enterprise < ActiveRecord::Base before_validation :initialize_permalink, if: lambda { permalink.nil? } before_validation :ensure_owner_is_manager, if: lambda { owner_id_changed? && !owner_id.nil? } + before_validation :ensure_email_set before_validation :set_unused_address_fields after_validation :geocode_address @@ -164,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 } @@ -246,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 @@ -394,6 +409,10 @@ class Enterprise < ActiveRecord::Base users << owner unless users.include?(owner) || owner.admin? end + def ensure_email_set + self.email = owner.email if email.blank? && owner.present? + end + def enforce_ownership_limit unless owner.can_own_more_enterprises? errors.add(:owner, "^#{owner.email} is not permitted to own any more enterprises (limit is #{owner.enterprise_limit}).") 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/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/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/enterprise_serializer.rb b/app/serializers/api/enterprise_serializer.rb index 287ebfe872..3210c1d2af 100644 --- a/app/serializers/api/enterprise_serializer.rb +++ b/app/serializers/api/enterprise_serializer.rb @@ -41,7 +41,7 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer attributes :name, :id, :description, :latitude, :longitude, :long_description, :website, :instagram, :linkedin, :twitter, :facebook, :is_primary_producer, :is_distributor, :phone, :visible, - :email, :hash, :logo, :promo_image, :path, :pickup, :delivery, + :email_address, :hash, :logo, :promo_image, :path, :pickup, :delivery, :icon, :icon_font, :producer_icon_font, :category, :producers, :hubs attributes :taxons, :supplied_taxons @@ -67,8 +67,8 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer services ? services[:delivery] : false end - def email - object.email.to_s.reverse + def email_address + object.email_address.to_s.reverse end def hash 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/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/index.html.haml b/app/views/admin/enterprise_groups/index.html.haml index 13bc19e364..e78dadc72c 100644 --- a/app/views/admin/enterprise_groups/index.html.haml +++ b/app/views/admin/enterprise_groups/index.html.haml @@ -1,9 +1,9 @@ = content_for :page_title do Enterprise Groups -= content_for :page_actions do - %li#new_enterprise_group_link - = button_link_to "New Enterprise Group", main_app.new_admin_enterprise_group_path, :icon => 'add', :id => 'admin_new_enterprise_group_link' +- if admin_user? + = content_for :page_actions do + %li= button_link_to "New Enterprise Group", main_app.new_admin_enterprise_group_path %table.index#listing_enterprise_groups %thead 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/_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 3ab9392712..ba8fce56ae 100644 --- a/app/views/admin/enterprises/_new_form.html.haml +++ b/app/views/admin/enterprises/_new_form.html.haml @@ -1,6 +1,7 @@ .row .three.columns.alpha = f.label :name + %span.required * .nine.columns.omega = f.text_field :name, { placeholder: "eg. Professor Plum's Biodynamic Truffles", class: "fullwidth" } @@ -8,6 +9,7 @@ .row .three.columns.alpha =f.label :owner_id, 'Owner' + %span.required * %div{'ofn-with-tip' => "The primary user responsible for this enterprise."} %a What's this? .nine.columns.omega @@ -50,13 +52,9 @@ = f.text_field :contact, { placeholder: "eg. Gustav Plum"} .row .alpha.three.columns - = f.label :email + = f.label :email_address .omega.nine.columns - = f.text_field :email, { placeholder: "eg. gustav@truffles.com", "ng-model" => "Enterprise.email" } - .alert-box - %i.icon-info-sign - If we don't recognise this email address we'll send you a confirmation email to make sure it belongs to you. You'll need to use the link in the email we send to fully activate your new enterprise. - %a.close{ href: "" } × + = f.text_field :email_address, { placeholder: "eg. gustav@truffles.com", "ng-model" => "Enterprise.email_address" } .row .alpha.three.columns = f.label :phone @@ -72,6 +70,7 @@ .row .three.columns.alpha = af.label :address1 + %span.required * .nine.columns.omega = af.text_field :address1, { placeholder: "eg. 123 High Street"} .row @@ -84,6 +83,7 @@ = af.label :city, 'Suburb' \/ = af.label :zipcode, 'Postcode' + %span.required * .four.columns = af.text_field :city, { placeholder: "eg. Northcote"} .five.columns.omega diff --git a/app/views/admin/enterprises/form/_contact.html.haml b/app/views/admin/enterprises/form/_contact.html.haml index df28b6a921..28bbd6014b 100644 --- a/app/views/admin/enterprises/form/_contact.html.haml +++ b/app/views/admin/enterprises/form/_contact.html.haml @@ -1,11 +1,3 @@ --if @enterprise.pending_any_confirmation? - .alert-box - - email = @enterprise.confirmed? ? @enterprise.unconfirmed_email : @enterprise.email - Email confirmation is pending. - We've sent a confirmation email to - %strong= "#{email}." - = link_to('Resend', main_app.enterprise_confirmation_path(enterprise: { id: @enterprise.id, email: email } ), method: :post) - %a.close{ href: "#" } × .row .alpha.three.columns = f.label :contact, 'Name' @@ -13,15 +5,9 @@ = f.text_field :contact, { placeholder: "eg. Gustav Plum"} .row .alpha.three.columns - = f.label :email - %span.required * + = f.label :email_address .omega.eight.columns - = f.text_field :email, { placeholder: "eg. gustav@truffles.com", "ng-model" => "Enterprise.email" } -.row{ ng: { hide: "pristineEmail == null || pristineEmail == Enterprise.email"} } - .alpha.three.columns -   - .omega.eight.columns - Note: A new email address may need to be confirmed prior to use + = f.text_field :email_address, { placeholder: "eg. gustav@truffles.com" } .row .alpha.three.columns = f.label :phone 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/_users.html.haml b/app/views/admin/enterprises/form/_users.html.haml index 289a85d7a1..42e25fad0a 100644 --- a/app/views/admin/enterprises/form/_users.html.haml +++ b/app/views/admin/enterprises/form/_users.html.haml @@ -1,6 +1,15 @@ - owner_email = @enterprise.andand.owner.andand.email || "" - full_permissions = (spree_current_user.admin? || spree_current_user == @enterprise.andand.owner) +-if @enterprise.pending_any_confirmation? + .alert-box + - email = @enterprise.confirmed? ? @enterprise.unconfirmed_email : @enterprise.email + Email confirmation is pending. + We've sent a confirmation email to + %strong= "#{email}." + = link_to('Resend', main_app.enterprise_confirmation_path(enterprise: { id: @enterprise.id, email: email } ), method: :post) + %a.close{ href: "#" } × + .row .three.columns.alpha =f.label :owner_id, 'Owner' @@ -14,6 +23,24 @@ - else = owner_email +.row + .three.columns.alpha + = f.label :email, 'Notifications' + - if full_permissions + %span.required * + .with-tip{'data-powertip' => "Notifications about orders will be send to this email address."} + %a What's this? + .eight.columns.omega + - if full_permissions + = f.text_field :email, { placeholder: "eg. gustav@truffles.com", "ng-model" => "Enterprise.email" } + - else + = @enterprise.email +.row{ ng: { hide: "pristineEmail == null || pristineEmail == Enterprise.email"} } + .alpha.three.columns +   + .omega.eight.columns + Note: A new email address may need to be confirmed prior to use + .row .three.columns.alpha =f.label :user_ids, 'Managers' 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/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/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 d5f8bb9714..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', 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/json/partials/_enterprise.rabl b/app/views/json/partials/_enterprise.rabl index 6e17cf3c58..ca99b9d14c 100644 --- a/app/views/json/partials/_enterprise.rabl +++ b/app/views/json/partials/_enterprise.rabl @@ -1,7 +1,7 @@ attributes :name, :id, :description, :latitude, :longitude, :long_description, :website, :instagram, :linkedin, :twitter, :facebook, :is_primary_producer, :is_distributor, :phone -node :email do |enterprise| - enterprise.email.to_s.reverse +node :email_address do |enterprise| + enterprise.email_address.to_s.reverse end child :address do diff --git a/app/views/order_cycles/_orders_closed.html.haml b/app/views/order_cycles/_orders_closed.html.haml index ae0568ae2d..782c680add 100644 --- a/app/views/order_cycles/_orders_closed.html.haml +++ b/app/views/order_cycles/_orders_closed.html.haml @@ -12,6 +12,6 @@ = t :ocs_closed_opens, time: distance_of_time_in_words_to_now(next_oc.orders_open_at) %p - = t(:ocs_closed_email, email: current_distributor.email) if current_distributor.email + = t(:ocs_closed_email, email: current_distributor.email_address) if current_distributor.email_address %br/ = t(:ocs_closed_phone, phone: current_distributor.phone) if current_distributor.phone diff --git a/app/views/producers/_fat.html.haml b/app/views/producers/_fat.html.haml index c1de090978..9faa239c02 100644 --- a/app/views/producers/_fat.html.haml +++ b/app/views/producers/_fat.html.haml @@ -23,7 +23,7 @@ %div.show-for-medium-up{"ng-if" => "producer.supplied_taxons.length==0"}   - %div{"bo-if" => "producer.email || producer.website || producer.phone"} + %div{"bo-if" => "producer.email_address || producer.website || producer.phone"} %label = t :producers_contact @@ -31,9 +31,9 @@ = t :producers_contact_phone %span{"bo-text" => "producer.phone"} - %p.word-wrap{"bo-if" => "producer.email"} - %a{"bo-href" => "producer.email | stripUrl", target: "_blank", mailto: true} - %span.email{"bo-bind" => "producer.email | stripUrl"} + %p.word-wrap{"bo-if" => "producer.email_address"} + %a{"bo-href" => "producer.email_address | stripUrl", target: "_blank", mailto: true} + %span.email{"bo-bind" => "producer.email_address | stripUrl"} %p.word-wrap{"bo-if" => "producer.website"} %a{"bo-href-i" => "http://{{producer.website | stripUrl}}", 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/shopping_shared/_contact.html.haml b/app/views/shopping_shared/_contact.html.haml index a88c421270..100d64d958 100644 --- a/app/views/shopping_shared/_contact.html.haml +++ b/app/views/shopping_shared/_contact.html.haml @@ -18,7 +18,7 @@ = current_distributor.address.zipcode .small-12.large-4.columns - - if current_distributor.website || current_distributor.email + - if current_distributor.website || current_distributor.email_address %div.center .header = t :shopping_contact_web @@ -27,10 +27,10 @@ %a{href: "http://#{current_distributor.website}", target: "_blank" } = current_distributor.website %br - - unless current_distributor.email.blank? - %a{href: current_distributor.email.reverse, mailto: true} + - unless current_distributor.email_address.blank? + %a{href: current_distributor.email_address.reverse, mailto: true} %span.email - = current_distributor.email.reverse + = current_distributor.email_address.reverse .small-12.large-4.columns - if current_distributor.twitter.present? || current_distributor.facebook.present? || current_distributor.linkedin.present? || current_distributor.instagram.present? diff --git a/app/views/spree/admin/orders/bulk_management.html.haml b/app/views/spree/admin/orders/bulk_management.html.haml index 46e5caf191..003c1ab5f8 100644 --- a/app/views/spree/admin/orders/bulk_management.html.haml +++ b/app/views/spree/admin/orders/bulk_management.html.haml @@ -92,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/config/locales/en.yml b/config/locales/en.yml index a5c5ba7903..323288edc2 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" @@ -555,8 +602,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using who_is_managing_enterprise: "Who is responsible for managing %{enterprise}?" enterprise_contact: "Primary Contact" enterprise_contact_required: "You need to enter a primary contact." - enterprise_email: "Email address" - enterprise_email_required: "You need to enter valid email address." + enterprise_email_address: "Email address" enterprise_phone: "Phone number" back: "Back" continue: "Continue" diff --git a/config/routes.rb b/config/routes.rb index 62c4153d14..9821a8b4bf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -104,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/20160205044930_add_email_address_to_enterprises.rb b/db/migrate/20160205044930_add_email_address_to_enterprises.rb new file mode 100644 index 0000000000..b1047fe4b8 --- /dev/null +++ b/db/migrate/20160205044930_add_email_address_to_enterprises.rb @@ -0,0 +1,5 @@ +class AddEmailAddressToEnterprises < ActiveRecord::Migration + def change + add_column :enterprises, :email_address, :string + end +end diff --git a/db/migrate/20160212092908_set_enterprise_email_address.rb b/db/migrate/20160212092908_set_enterprise_email_address.rb new file mode 100644 index 0000000000..a43591624a --- /dev/null +++ b/db/migrate/20160212092908_set_enterprise_email_address.rb @@ -0,0 +1,8 @@ +class SetEnterpriseEmailAddress < ActiveRecord::Migration + def up + Enterprise.all.each do |enterprise| + enterprise.email_address = enterprise.email + enterprise.save + end + 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..c84337565a 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" @@ -346,6 +347,7 @@ ActiveRecord::Schema.define(:version => 20151128185900) do t.boolean "producer_profile_only", :default => false t.string "permalink", :null => false t.boolean "charges_sales_tax", :default => false, :null => false + t.string "email_address" end add_index "enterprises", ["address_id"], :name => "index_enterprises_on_address_id" @@ -394,6 +396,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 +682,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 +1167,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/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 dd8d02e7bf..21c6cecd15 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 d28a20d6f9..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" 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/enterprises_spec.rb b/spec/features/admin/enterprises_spec.rb index aed96b5a91..46a0105bb4 100644 --- a/spec/features/admin/enterprises_spec.rb +++ b/spec/features/admin/enterprises_spec.rb @@ -47,7 +47,7 @@ feature %q{ fill_in 'enterprise_contact', :with => 'Kirsten or Ren' fill_in 'enterprise_phone', :with => '0413 897 321' - fill_in 'enterprise_email', :with => 'info@eaterprises.com.au' + fill_in 'enterprise_email_address', :with => 'info@eaterprises.com.au' fill_in 'enterprise_website', :with => 'http://eaterprises.com.au' fill_in 'enterprise_address_attributes_address1', :with => '35 Ballantyne St' @@ -130,7 +130,7 @@ feature %q{ click_link "Contact" fill_in 'enterprise_contact', :with => 'Kirsten or Ren' fill_in 'enterprise_phone', :with => '0413 897 321' - fill_in 'enterprise_email', :with => 'info@eaterprises.com.au' + fill_in 'enterprise_email_address', :with => 'info@eaterprises.com.au' fill_in 'enterprise_website', :with => 'http://eaterprises.com.au' click_link "Social" @@ -286,7 +286,7 @@ feature %q{ click_link 'Enterprises' click_link 'New Enterprise' fill_in 'enterprise_name', with: 'zzz' - fill_in 'enterprise_email', with: 'bob@example.com' + fill_in 'enterprise_email_address', with: 'bob@example.com' fill_in 'enterprise_address_attributes_address1', with: 'z' fill_in 'enterprise_address_attributes_city', with: 'z' fill_in 'enterprise_address_attributes_zipcode', with: 'z' diff --git a/spec/features/admin/payment_method_spec.rb b/spec/features/admin/payment_method_spec.rb index aa7caa2e0b..5c16f886e5 100644 --- a/spec/features/admin/payment_method_spec.rb +++ b/spec/features/admin/payment_method_spec.rb @@ -44,7 +44,7 @@ feature %q{ select2_select "PayPal Express", from: "payment_method_type" click_button 'Update' - expect(flash_message).to eq 'Payment Method has been successfully updated!' + flash_message.should eq 'Payment Method has been successfully updated!' payment_method = Spree::PaymentMethod.find_by_name('New PM Name') expect(payment_method.distributors).to include @distributors[1], @distributors[2] diff --git a/spec/features/admin/variant_overrides_spec.rb b/spec/features/admin/variant_overrides_spec.rb index 378afa3626..6f59b78aec 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 have_no_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 have_no_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 have_no_selector "tr#v_#{variant.id}" + expect(page).to have_no_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 have_no_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 have_no_selector "tr#v_#{variant_related.id}" + within "tr#v_#{variant.id}" do click_button 'Add' end + expect(page).to have_no_selector "tr#v_#{variant.id}" + expect(page).to have_no_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 have_no_selector "table#variant-overrides tr#v_#{variant1.id}" + expect(page).to have_no_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 have_no_selector "table#new-products tr#v_#{variant1.id}" + expect(page).to have_no_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 have_no_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 have_no_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/features/consumer/registration_spec.rb b/spec/features/consumer/registration_spec.rb index d36cd48f7d..0e54011f1c 100644 --- a/spec/features/consumer/registration_spec.rb +++ b/spec/features/consumer/registration_spec.rb @@ -38,7 +38,7 @@ feature "Registration", js: true do # Filling in Contact Details expect(page).to have_content 'Who is responsible for managing My Awesome Enterprise?' fill_in 'enterprise_contact', with: 'Saskia Munroe' - page.should have_field 'enterprise_email', with: user.email + page.should have_field 'enterprise_email_address', with: user.email fill_in 'enterprise_phone', with: '12 3456 7890' click_button 'Continue' 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 "