mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
Auto-merged master into uk/account-balances on deployment.
This commit is contained in:
6
Gemfile
6
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
|
||||
|
||||
10
Gemfile.lock
10
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)
|
||||
|
||||
BIN
app/assets/images/select2.png
Executable file
BIN
app/assets/images/select2.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
app/assets/images/select2x2.png
Executable file
BIN
app/assets/images/select2x2.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -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
|
||||
|
||||
@@ -1,5 +1,2 @@
|
||||
angular.module("ofn.admin").controller "enterprisesDashboardCtrl", [
|
||||
"$scope"
|
||||
($scope) ->
|
||||
$scope.activeTab = "hubs"
|
||||
]
|
||||
angular.module("ofn.admin").controller "enterprisesDashboardCtrl", ($scope) ->
|
||||
$scope.activeTab = "hubs"
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
angular.module("admin.dropdown").controller "DropDownCtrl", ($scope) ->
|
||||
$scope.expanded = false
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = '<a href="'+url+'" class="delete-resource icon_link with-tip icon-trash no-text" data-action="remove" data-confirm="Are you sure?" url="'+url+'"></a>';
|
||||
//var html = '<a href="'+url+'" class="delete-resource" data-confirm="Are you sure?"><img alt="Delete" src="/assets/admin/icons/delete.png" /> Delete</a>';
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 = '<a href="' + url + '" class="delete-resource icon_link icon-trash no-text" data-action="remove" data-confirm="Are you sure?" url="' + url + '"></a>'
|
||||
#var html = '<a href="'+url+'" class="delete-resource" data-confirm="Are you sure?"><img alt="Delete" src="/assets/admin/icons/delete.png" /> Delete</a>';
|
||||
element.append html
|
||||
return
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
angular.module("admin.enterpriseFees", ['admin.indexUtils'])
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "<option value=''>#{attrs.includeBlank}</option>" else ""
|
||||
return "<select ng-options='e.#{valueAttr} as e.#{textAttr} for e in data'>#{blank}</select>"
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -1 +1 @@
|
||||
angular.module("admin.indexUtils", ['ngResource', 'templates']).config ($httpProvider) ->
|
||||
angular.module("admin.indexUtils", ['ngResource', 'ngSanitize', 'templates']).config ($httpProvider) ->
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
angular.module("admin.inventoryItems", ['ngResource'])
|
||||
@@ -0,0 +1,5 @@
|
||||
angular.module("admin.inventoryItems").factory 'InventoryItemResource', ($resource) ->
|
||||
$resource('/admin/inventory_items/:id/:action.json', {}, {
|
||||
'update':
|
||||
method: 'PUT'
|
||||
})
|
||||
@@ -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
|
||||
@@ -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])
|
||||
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
angular.module("admin.taxons", [])
|
||||
angular.module("admin.taxons", ['ngSanitize'])
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
angular.module("admin.users", [])
|
||||
angular.module("admin.users", ['admin.utils'])
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1 +1 @@
|
||||
angular.module("admin.utils", [])
|
||||
angular.module("admin.utils", ["ngSanitize"])
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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"])
|
||||
|
||||
@@ -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()" } }
|
||||
×
|
||||
19
app/assets/stylesheets/admin/advanced_settings.css.scss
Normal file
19
app/assets/stylesheets/admin/advanced_settings.css.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
app/assets/stylesheets/admin/components/alert_row.css.scss
Normal file
21
app/assets/stylesheets/admin/components/alert_row.css.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
7
app/assets/stylesheets/admin/offsets.css.scss
Normal file
7
app/assets/stylesheets/admin/offsets.css.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.margin-bottom-20 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.margin-bottom-50 {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
10
app/assets/stylesheets/admin/select2.css.scss
Normal file
10
app/assets/stylesheets/admin/select2.css.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
.select2-container {
|
||||
.select2-choice {
|
||||
.select2-arrow {
|
||||
width: 22px;
|
||||
border: none;
|
||||
background-image: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
app/assets/stylesheets/admin/typography.css.scss
Normal file
9
app/assets/stylesheets/admin/typography.css.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
.text-normal {
|
||||
font-size: 1.0rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.text-big {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
.variant-override-unit
|
||||
float: right
|
||||
font-style: italic
|
||||
|
||||
button.hide:hover
|
||||
background-color: #DA5354
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
28
app/controllers/admin/inventory_items_controller.rb
Normal file
28
app/controllers/admin/inventory_items_controller.rb
Normal file
@@ -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
|
||||
@@ -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} }
|
||||
|
||||
@@ -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
|
||||
|
||||
55
app/controllers/discourse_sso_controller.rb
Normal file
55
app/controllers/discourse_sso_controller.rb
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
%(<option value="#{ERB::Util.html_escape(value)}"#{selected_attribute}#{html_attributes}>#{ERB::Util.html_escape(text)}</option>)
|
||||
%(<option value="#{ERB::Util.html_escape(value)}"#{html_attributes}>#{ERB::Util.html_escape(text)}</option>)
|
||||
end.join("\n").html_safe
|
||||
end
|
||||
|
||||
|
||||
25
app/helpers/discourse_helper.rb
Normal file
25
app/helpers/discourse_helper.rb
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
14
app/models/inventory_item.rb
Normal file
14
app/models/inventory_item.rb
Normal file
@@ -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
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
40
app/models/spree/calculator/default_tax_decorator.rb
Normal file
40
app/models/spree/calculator/default_tax_decorator.rb
Normal file
@@ -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
|
||||
@@ -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) }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
2
app/overrides/spree/admin/orders/index/set_ng_app.deface
Normal file
2
app/overrides/spree/admin/orders/index/set_ng_app.deface
Normal file
@@ -0,0 +1,2 @@
|
||||
add_to_attributes "table#listing_orders"
|
||||
attributes "ng-app" => "ofn.admin"
|
||||
@@ -1,4 +1,7 @@
|
||||
/ replace_contents "title"
|
||||
|
||||
= t(controller.controller_name, :default => controller.controller_name.titleize)
|
||||
= " - OFN #{t(:administration)}"
|
||||
- if content_for? :html_title
|
||||
= yield :html_title
|
||||
- else
|
||||
= t(controller.controller_name, :default => controller.controller_name.titleize)
|
||||
= " - OFN #{t(:administration)}"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<!-- surround_contents 'body' -->
|
||||
|
||||
<div <%= yield(:app_wrapper_attrs).strip.html_safe %>>
|
||||
<%= render_original %>
|
||||
</div>
|
||||
@@ -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
|
||||
11
app/serializers/api/admin/calculator_serializer.rb
Normal file
11
app/serializers/api/admin/calculator_serializer.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
class Api::Admin::CalculatorSerializer < ActiveModel::Serializer
|
||||
attributes :name, :description
|
||||
|
||||
def name
|
||||
object.name
|
||||
end
|
||||
|
||||
def description
|
||||
object.description
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
3
app/serializers/api/admin/inventory_item_serializer.rb
Normal file
3
app/serializers/api/admin/inventory_item_serializer.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class Api::Admin::InventoryItemSerializer < ActiveModel::Serializer
|
||||
attributes :id, :enterprise_id, :variant_id, :visible
|
||||
end
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' } }
|
||||
|
||||
3
app/views/admin/enterprise_fees/_data.html.haml
Normal file
3
app/views/admin/enterprise_fees/_data.html.haml
Normal file
@@ -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
|
||||
@@ -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"}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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?
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user