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