Auto-merged master into uk/account-balances on deployment.

This commit is contained in:
Maikel
2016-03-09 14:43:05 +11:00
194 changed files with 2970 additions and 973 deletions

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
app/assets/images/select2x2.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -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

View File

@@ -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"

View File

@@ -1,2 +0,0 @@
angular.module("admin.dropdown").controller "DropDownCtrl", ($scope) ->
$scope.expanded = false

View File

@@ -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()

View File

@@ -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

View File

@@ -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);
}
});
}
});

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1 @@
angular.module("admin.enterpriseFees", ['admin.indexUtils'])

View File

@@ -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"

View File

@@ -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>"

View File

@@ -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 }

View File

@@ -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

View File

@@ -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

View File

@@ -1 +1 @@
angular.module("admin.indexUtils", ['ngResource', 'templates']).config ($httpProvider) ->
angular.module("admin.indexUtils", ['ngResource', 'ngSanitize', 'templates']).config ($httpProvider) ->

View File

@@ -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

View File

@@ -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)

View File

@@ -0,0 +1 @@
angular.module("admin.inventoryItems", ['ngResource'])

View File

@@ -0,0 +1,5 @@
angular.module("admin.inventoryItems").factory 'InventoryItemResource', ($resource) ->
$resource('/admin/inventory_items/:id/:action.json', {}, {
'update':
method: 'PUT'
})

View File

@@ -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

View File

@@ -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])

View File

@@ -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)

View File

@@ -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])

View File

@@ -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"

View File

@@ -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

View File

@@ -1 +1 @@
angular.module("admin.taxons", [])
angular.module("admin.taxons", ['ngSanitize'])

View File

@@ -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

View File

@@ -1 +1 @@
angular.module("admin.users", [])
angular.module("admin.users", ['admin.utils'])

View File

@@ -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

View File

@@ -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

View File

@@ -1 +1 @@
angular.module("admin.utils", [])
angular.module("admin.utils", ["ngSanitize"])

View File

@@ -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"

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"])

View File

@@ -0,0 +1,8 @@
.sixteen.columns.alpha.omega.alert-row{ ng: { show: '!dismissed' } }
.fifteen.columns.pad.alpha
%span.message.text-big{ ng: { bind: 'message'} }
&nbsp;&nbsp;&nbsp;
%input{ type: 'button', ng: { value: "buttonText", show: 'buttonText && buttonAction', click: "buttonAction()" } }
.one.column.omega.pad.text-center
%a.close{ href: "#", ng: { click: "dismiss()" } }
&times;

View 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;
}
}
}

View 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;
}
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -0,0 +1,7 @@
.margin-bottom-20 {
margin-bottom: 20px;
}
.margin-bottom-50 {
margin-bottom: 50px;
}

View File

@@ -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;

View File

@@ -0,0 +1,10 @@
.select2-container {
.select2-choice {
.select2-arrow {
width: 22px;
border: none;
background-image: none;
background-color: transparent;
}
}
}

View File

@@ -0,0 +1,9 @@
.text-normal {
font-size: 1.0rem;
font-weight: 300;
}
.text-big {
font-size: 1.2rem;
font-weight: 300;
}

View File

@@ -1,3 +1,6 @@
.variant-override-unit
float: right
font-style: italic
button.hide:hover
background-color: #DA5354

View File

@@ -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

View File

@@ -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).

View 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

View File

@@ -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} }

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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).

View File

@@ -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

View 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

View File

@@ -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) }

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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/

View File

@@ -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

View File

@@ -0,0 +1,2 @@
add_to_attributes "table#listing_orders"
attributes "ng-app" => "ofn.admin"

View File

@@ -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)}"

View File

@@ -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'

View File

@@ -0,0 +1,5 @@
<!-- surround_contents 'body' -->
<div <%= yield(:app_wrapper_attrs).strip.html_safe %>>
<%= render_original %>
</div>

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,3 @@
class Api::Admin::InventoryItemSerializer < ActiveModel::Serializer
attributes :id, :enterprise_id, :variant_id, :visible
end

View File

@@ -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

View File

@@ -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

View File

@@ -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' } }

View 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

View File

@@ -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"}

View File

@@ -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

View File

@@ -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