Merge branch master into properties

This commit is contained in:
Rob Harrington
2015-04-17 13:23:33 +10:00
243 changed files with 6059 additions and 1487 deletions

2
.mailmap Normal file
View File

@@ -0,0 +1,2 @@
Rob Harrington <oeoeaio@gmail.com>
Laura Summers <summerscope@gmail.com>

View File

@@ -15,6 +15,8 @@ gem 'spree_auth_devise', :github => 'spree/spree_auth_devise', :branch => '1-3-s
gem 'spree_paypal_express', :github => "openfoodfoundation/better_spree_paypal_express", :branch => "1-3-stable"
#gem 'spree_paypal_express', :github => "spree-contrib/better_spree_paypal_express", :branch => "1-3-stable"
gem 'delayed_job_active_record'
gem 'daemons'
gem 'comfortable_mexican_sofa'
# Fix bug in simple_form preventing collection_check_boxes usage within form_for block

View File

@@ -217,6 +217,7 @@ GEM
safe_yaml (~> 0.9.0)
css_parser (1.3.5)
addressable
daemons (1.2.2)
dalli (2.7.2)
database_cleaner (0.7.1)
db2fog (0.8.0)
@@ -229,6 +230,11 @@ GEM
debugger-ruby_core_source (~> 1.2.3)
debugger-linecache (1.2.0)
debugger-ruby_core_source (1.2.3)
delayed_job (4.0.4)
activesupport (>= 3.0, < 4.2)
delayed_job_active_record (4.0.2)
activerecord (>= 3.0, < 4.2)
delayed_job (>= 3.0, < 4.1)
devise (2.2.8)
bcrypt-ruby (~> 3.0)
orm_adapter (~> 0.1)
@@ -334,7 +340,7 @@ GEM
mail (2.5.4)
mime-types (~> 1.16)
treetop (~> 1.4.8)
method_source (0.8.1)
method_source (0.8.2)
mime-types (1.25.1)
mini_portile (0.6.2)
momentjs-rails (2.5.1)
@@ -518,7 +524,7 @@ GEM
xml-simple (1.1.4)
xpath (2.0.0)
nokogiri (~> 1.3)
zeus (0.13.3)
zeus (0.15.4)
method_source (>= 0.6.7)
PLATFORMS
@@ -538,11 +544,13 @@ DEPENDENCIES
comfortable_mexican_sofa
compass-rails
custom_error_message!
daemons
dalli
database_cleaner (= 0.7.1)
db2fog
debugger-linecache
deface!
delayed_job_active_record
factory_girl_rails
figaro
foreigner

View File

@@ -95,4 +95,4 @@ usage instructions.
## Licence
Copyright (c) 2012 - 2013 Open Food Foundation, released under the AGPL licence.
Copyright (c) 2012 - 2015 Open Food Foundation, released under the AGPL licence.

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -19,6 +19,7 @@
//= require ../shared/ng-infinite-scroll.min.js
//= require ./admin
//= require ./enterprises/enterprises
//= require ./enterprise_groups/enterprise_groups
//= require ./payment_methods/payment_methods
//= require ./products/products
//= require ./shipping_methods/shipping_methods

View File

@@ -0,0 +1,3 @@
angular.module("admin.enterprise_groups")
.controller "enterpriseGroupCtrl", ($scope, SideMenu) ->
$scope.menu = SideMenu

View File

@@ -0,0 +1,15 @@
angular.module("admin.enterprise_groups")
.controller "sideMenuCtrl", ($scope, SideMenu) ->
$scope.menu = SideMenu
$scope.select = SideMenu.select
$scope.menu.setItems [
{ name: 'Primary Details', icon_class: "icon-user" }
{ name: 'Users', icon_class: "icon-user" }
{ name: 'About', icon_class: "icon-pencil" }
{ name: 'Images', icon_class: "icon-picture" }
{ name: 'Contact', icon_class: "icon-phone" }
{ name: 'Web', icon_class: "icon-globe" }
]
$scope.select(0)

View File

@@ -0,0 +1 @@
angular.module("admin.enterprise_groups", ["admin.side_menu", "admin.users", "textAngular"])

View File

@@ -13,6 +13,7 @@ angular.module("admin.enterprises")
{ name: 'About', icon_class: "icon-pencil" }
{ name: 'Business Details', icon_class: "icon-briefcase" }
{ name: 'Images', icon_class: "icon-picture" }
{ name: "Properties", icon_class: "icon-tags", show: "showProperties()" }
{ 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()" }
@@ -28,6 +29,9 @@ angular.module("admin.enterprises")
else
true
$scope.showProperties = ->
!!$scope.Enterprise.is_primary_producer
$scope.showShippingMethods = ->
enterprisePermissions.can_manage_shipping_methods && $scope.Enterprise.sells != "none"

View File

@@ -1,10 +1,10 @@
angular.module('admin.order_cycles', ['ngResource'])
.controller('AdminCreateOrderCycleCtrl', ['$scope', 'OrderCycle', 'Enterprise', 'EnterpriseFee', ($scope, OrderCycle, Enterprise, EnterpriseFee) ->
$scope.enterprises = Enterprise.index()
.controller('AdminCreateOrderCycleCtrl', ['$scope', '$filter', 'OrderCycle', 'Enterprise', 'EnterpriseFee', 'ocInstance', ($scope, $filter, OrderCycle, Enterprise, EnterpriseFee, ocInstance) ->
$scope.enterprises = Enterprise.index(coordinator_id: ocInstance.coordinator_id)
$scope.supplied_products = Enterprise.supplied_products
$scope.enterprise_fees = EnterpriseFee.index()
$scope.enterprise_fees = EnterpriseFee.index(coordinator_id: ocInstance.coordinator_id)
$scope.order_cycle = OrderCycle.order_cycle
$scope.order_cycle = OrderCycle.new({ coordinator_id: ocInstance.coordinator_id})
$scope.loaded = ->
Enterprise.loaded && EnterpriseFee.loaded
@@ -27,14 +27,14 @@ angular.module('admin.order_cycles', ['ngResource'])
$scope.variantSuppliedToOrderCycle = (variant) ->
OrderCycle.variantSuppliedToOrderCycle(variant)
$scope.incomingExchangesVariants = ->
OrderCycle.incomingExchangesVariants()
$scope.incomingExchangeVariantsFor = (enterprise_id) ->
$filter('filterExchangeVariants')(OrderCycle.incomingExchangesVariants(), $scope.order_cycle.visible_variants_for_outgoing_exchanges[enterprise_id])
$scope.exchangeDirection = (exchange) ->
OrderCycle.exchangeDirection(exchange)
$scope.participatingEnterprises = ->
$scope.enterprises[id] for id in OrderCycle.participatingEnterpriseIds()
$scope.enterprisesWithFees = ->
$scope.enterprises[id] for id in OrderCycle.participatingEnterpriseIds() when $scope.enterpriseFeesForEnterprise(id).length > 0
$scope.toggleProducts = ($event, exchange) ->
$event.preventDefault()
@@ -79,12 +79,12 @@ angular.module('admin.order_cycles', ['ngResource'])
OrderCycle.create()
])
.controller('AdminEditOrderCycleCtrl', ['$scope', '$location', 'OrderCycle', 'Enterprise', 'EnterpriseFee', ($scope, $location, OrderCycle, Enterprise, EnterpriseFee) ->
$scope.enterprises = Enterprise.index()
$scope.supplied_products = Enterprise.supplied_products
$scope.enterprise_fees = EnterpriseFee.index()
.controller('AdminEditOrderCycleCtrl', ['$scope', '$filter', '$location', 'OrderCycle', 'Enterprise', 'EnterpriseFee', ($scope, $filter, $location, OrderCycle, Enterprise, EnterpriseFee) ->
order_cycle_id = $location.absUrl().match(/\/admin\/order_cycles\/(\d+)/)[1]
$scope.enterprises = Enterprise.index(order_cycle_id: order_cycle_id)
$scope.supplied_products = Enterprise.supplied_products
$scope.enterprise_fees = EnterpriseFee.index(order_cycle_id: order_cycle_id)
$scope.order_cycle = OrderCycle.load(order_cycle_id)
$scope.loaded = ->
@@ -108,14 +108,14 @@ angular.module('admin.order_cycles', ['ngResource'])
$scope.variantSuppliedToOrderCycle = (variant) ->
OrderCycle.variantSuppliedToOrderCycle(variant)
$scope.incomingExchangesVariants = ->
OrderCycle.incomingExchangesVariants()
$scope.incomingExchangeVariantsFor = (enterprise_id) ->
$filter('filterExchangeVariants')(OrderCycle.incomingExchangesVariants(), $scope.order_cycle.visible_variants_for_outgoing_exchanges[enterprise_id])
$scope.exchangeDirection = (exchange) ->
OrderCycle.exchangeDirection(exchange)
$scope.participatingEnterprises = ->
$scope.enterprises[id] for id in OrderCycle.participatingEnterpriseIds()
$scope.enterprisesWithFees = ->
$scope.enterprises[id] for id in OrderCycle.participatingEnterpriseIds() when $scope.enterpriseFeesForEnterprise(id).length > 0
$scope.toggleProducts = ($event, exchange) ->
$event.preventDefault()

View File

@@ -1,8 +1,9 @@
angular.module('admin.order_cycles').controller "AdminSimpleCreateOrderCycleCtrl", ($scope, OrderCycle, Enterprise, EnterpriseFee) ->
$scope.enterprises = Enterprise.index (enterprises) =>
$scope.init(enterprises)
$scope.enterprise_fees = EnterpriseFee.index()
$scope.order_cycle = OrderCycle.order_cycle
angular.module('admin.order_cycles').controller "AdminSimpleCreateOrderCycleCtrl", ($scope, OrderCycle, Enterprise, EnterpriseFee, ocInstance) ->
$scope.order_cycle = OrderCycle.new {coordinator_id: ocInstance.coordinator_id}, =>
# TODO: make this a get method, which only fetches one enterprise
$scope.enterprises = Enterprise.index {coordinator_id: ocInstance.coordinator_id}, (enterprises) =>
$scope.init(enterprises)
$scope.enterprise_fees = EnterpriseFee.index(coordinator_id: ocInstance.coordinator_id)
$scope.init = (enterprises) ->
enterprise = enterprises[Object.keys(enterprises)[0]]

View File

@@ -2,8 +2,8 @@ angular.module('admin.order_cycles').controller "AdminSimpleEditOrderCycleCtrl",
$scope.orderCycleId = ->
$location.absUrl().match(/\/admin\/order_cycles\/(\d+)/)[1]
$scope.enterprises = Enterprise.index()
$scope.enterprise_fees = EnterpriseFee.index()
$scope.enterprises = Enterprise.index(order_cycle_id: $scope.orderCycleId())
$scope.enterprise_fees = EnterpriseFee.index(order_cycle_id: $scope.orderCycleId())
$scope.order_cycle = OrderCycle.load $scope.orderCycleId(), (order_cycle) =>
$scope.init()

View File

@@ -0,0 +1,6 @@
angular.module("admin.order_cycles").filter "filterExchangeVariants", ->
return (variants, rules) ->
if variants? && rules?
return (variant for variant in variants when variant in rules)
else
return []

View File

@@ -0,0 +1,4 @@
angular.module("admin.order_cycles").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

@@ -0,0 +1,3 @@
angular.module("admin.order_cycles").filter "visibleProducts", ($filter) ->
return (products, exchange, rules) ->
return (product for product in products when $filter('visibleProductVariants')(product, exchange, rules).length > 0)

View File

@@ -1,24 +1,28 @@
angular.module('admin.order_cycles').factory('Enterprise', ($resource) ->
Enterprise = $resource('/admin/enterprises/for_order_cycle/:enterprise_id.json', {}, {'index': {method: 'GET', isArray: true}})
Enterprise = $resource('/admin/enterprises/for_order_cycle/:enterprise_id.json', {}, {
'index':
method: 'GET'
isArray: true
params:
order_cycle_id: '@order_cycle_id'
coordinator_id: '@coordinator_id'
})
{
Enterprise: Enterprise
enterprises: {}
supplied_products: []
loaded: false
index: (callback=null) ->
service = this
Enterprise.index (data) ->
index: (params={}, callback=null) ->
Enterprise.index params, (data) =>
for enterprise in data
service.enterprises[enterprise.id] = enterprise
@enterprises[enterprise.id] = enterprise
for product in enterprise.supplied_products
service.supplied_products.push(product)
@supplied_products.push(product)
service.loaded = true
(callback || angular.noop)(service.enterprises)
@loaded = true
(callback || angular.noop)(@enterprises)
this.enterprises
@@ -40,4 +44,4 @@ angular.module('admin.order_cycles').factory('Enterprise', ($resource) ->
numVariants += if product.variants.length == 0 then 1 else product.variants.length
numVariants
})
})

View File

@@ -1,18 +1,23 @@
angular.module('admin.order_cycles').factory('EnterpriseFee', ($resource) ->
EnterpriseFee = $resource('/admin/enterprise_fees/:enterprise_fee_id.json', {}, {'index': {method: 'GET', isArray: true}})
EnterpriseFee = $resource('/admin/enterprise_fees/for_order_cycle/:enterprise_fee_id.json', {}, {
'index':
method: 'GET'
isArray: true
params:
order_cycle_id: '@order_cycle_id'
coordinator_id: '@coordinator_id'
})
{
EnterpriseFee: EnterpriseFee
enterprise_fees: {}
loaded: false
index: ->
service = this
EnterpriseFee.index (data) ->
service.enterprise_fees = data
service.loaded = true
index: (params={}) ->
EnterpriseFee.index params, (data) =>
@enterprise_fees = data
@loaded = true
forEnterprise: (enterprise_id) ->
enterprise_fee for enterprise_fee in this.enterprise_fees when enterprise_fee.enterprise_id == enterprise_id
enterprise_fee for enterprise_fee in @enterprise_fees when enterprise_fee.enterprise_id == enterprise_id
})

View File

@@ -1,14 +1,12 @@
angular.module('admin.order_cycles').factory('OrderCycle', ($resource, $window) ->
OrderCycle = $resource '/admin/order_cycles/:order_cycle_id.json', {}, {
OrderCycle = $resource '/admin/order_cycles/:action_name/:order_cycle_id.json', {}, {
'index': { method: 'GET', isArray: true}
'new' : { method: 'GET', params: { action_name: "new" } }
'create': { method: 'POST'}
'update': { method: 'PUT'}}
{
order_cycle:
incoming_exchanges: []
outgoing_exchanges: []
coordinator_fees: []
order_cycle: {}
loaded: false
@@ -24,7 +22,9 @@ angular.module('admin.order_cycles').factory('OrderCycle', ($resource, $window)
exchange.showProducts = !exchange.showProducts
setExchangeVariants: (exchange, variants, selected) ->
exchange.variants[variant] = selected for variant in variants
direction = if exchange.incoming then "incoming" else "outgoing"
editable = @order_cycle["editable_variants_for_#{direction}_exchanges"][exchange.enterprise_id] || []
exchange.variants[variant] = selected for variant in variants when variant in editable
addSupplier: (new_supplier_id) ->
this.order_cycle.incoming_exchanges.push({enterprise_id: new_supplier_id, incoming: true, active: true, variants: {}, enterprise_fees: []})
@@ -84,6 +84,20 @@ angular.module('admin.order_cycles').factory('OrderCycle', ($resource, $window)
for exchange in this.order_cycle.outgoing_exchanges
exchange.variants[variant_id] = false
new: (params, callback=null) ->
OrderCycle.new params, (oc) =>
delete oc.$promise
delete oc.$resolved
angular.extend(@order_cycle, oc)
@order_cycle.incoming_exchanges = []
@order_cycle.outgoing_exchanges = []
delete(@order_cycle.exchanges)
@loaded = true
(callback || angular.noop)(@order_cycle)
@order_cycle
load: (order_cycle_id, callback=null) ->
service = this
OrderCycle.get {order_cycle_id: order_cycle_id}, (oc) ->
@@ -127,6 +141,7 @@ angular.module('admin.order_cycles').factory('OrderCycle', ($resource, $window)
dataForSubmit: ->
data = this.deepCopy()
data = this.stripNonSubmittableAttributes(data)
data = this.removeInactiveExchanges(data)
data = this.translateCoordinatorFees(data)
data = this.translateExchangeFees(data)
@@ -147,6 +162,14 @@ angular.module('admin.order_cycles').factory('OrderCycle', ($resource, $window)
data
stripNonSubmittableAttributes: (order_cycle) ->
delete order_cycle.id
delete order_cycle.viewing_as_coordinator
delete order_cycle.editable_variants_for_incoming_exchanges
delete order_cycle.editable_variants_for_outgoing_exchanges
delete order_cycle.visible_variants_for_outgoing_exchanges
order_cycle
removeInactiveExchanges: (order_cycle) ->
order_cycle.incoming_exchanges =
(exchange for exchange in order_cycle.incoming_exchanges when exchange.active)

View File

@@ -11,9 +11,9 @@ $ ->
# Temporarily handles the cart showing stuff
$(document).ready ->
$('#cart_adjustments').hide()
$('.cart_adjustment').hide()
$('th.cart-adjustment-header a').click ->
$('#cart_adjustments').toggle()
$('td.cart-adjustments a').click ->
$('.cart_adjustment').toggle()
$(this).html('Item Handling Fees (included in item totals)')
false

View File

@@ -0,0 +1,12 @@
Darkswarm.controller "GroupEnterpriseNodeCtrl", ($scope, CurrentHub) ->
$scope.active = false
$scope.toggle = ->
$scope.active = !$scope.active
$scope.open = ->
$scope.active
$scope.current = ->
$scope.hub.id is CurrentHub.hub.id

View File

@@ -0,0 +1,12 @@
Darkswarm.controller "GroupEnterprisesCtrl", ($scope, Search, FilterSelectorsService) ->
$scope.totalActive = FilterSelectorsService.totalActive
$scope.clearAll = FilterSelectorsService.clearAll
$scope.filterText = FilterSelectorsService.filterText
$scope.FilterSelectorsService = FilterSelectorsService
$scope.query = Search.search()
$scope.activeTaxons = []
$scope.show_profiles = false
$scope.filtersActive = false
$scope.$watch "query", (query)->
Search.search query

View File

@@ -0,0 +1,14 @@
Darkswarm.controller "GroupPageCtrl", ($scope, group_enterprises, Enterprises, MapConfiguration, OfnMap) ->
$scope.Enterprises = Enterprises
group_enterprises_ids = group_enterprises.map (enterprise) =>
enterprise.id
is_in_group = (enterprise) ->
group_enterprises_ids.indexOf(enterprise.id) != -1
$scope.group_producers = Enterprises.producers.filter is_in_group
$scope.group_hubs = Enterprises.hubs.filter is_in_group
$scope.map = angular.copy MapConfiguration.options
$scope.mapMarkers = OfnMap.enterprise_markers group_enterprises

View File

@@ -1,3 +1,3 @@
Darkswarm.controller "MapCtrl", ($scope, MapConfiguration, OfnMap)->
$scope.OfnMap = OfnMap
$scope.map = MapConfiguration.options
$scope.map = angular.copy MapConfiguration.options

View File

@@ -3,7 +3,7 @@ Darkswarm.controller "RegistrationCtrl", ($scope, RegistrationService, Enterpris
$scope.enterprise = EnterpriseRegistrationService.enterprise
$scope.select = RegistrationService.select
$scope.steps = ['details','contact','type','about','images','social']
$scope.steps = ['details', 'contact', 'type', 'about', 'images', 'social']
$scope.countries = availableCountries

View File

@@ -1,10 +1,14 @@
Darkswarm.controller "TabsCtrl", ($scope, $rootScope, $location, OrderCycle) ->
Darkswarm.controller "TabsCtrl", ($scope, $rootScope, $location) ->
# Return active if supplied path matches url hash path.
$scope.active = (path)->
$location.hash() == path
# Toggle tab selected status by setting the url hash path.
# Select tab by setting the url hash path.
$scope.select = (path)->
$location.hash path
# Toggle tab selected status by setting the url hash path.
$scope.toggle = (path)->
if $scope.active(path)
$location.hash ""
else

View File

@@ -0,0 +1,8 @@
Darkswarm.directive "linkToService", ->
restrict: 'E'
replace: true
scope: {
ref: '='
service: '='
}
template: '<a href="{{ref | ext_url: service}}" target="_blank" ng-show="ref"></a>'

View File

@@ -0,0 +1,7 @@
Darkswarm.filter "ext_url", ->
urlPattern = /^https?:\/\//
(url, prefix) ->
if !url || url.match(urlPattern)
url
else
prefix + url

View File

@@ -1,9 +1,10 @@
Darkswarm.filter 'shipping', ()->
Darkswarm.filter 'shipping', ()->
(objects, options)->
objects ||= []
options ?= null
if options.pickup and !options.delivery
if !options
objects
else if options.pickup and !options.delivery
objects.filter (obj)->
obj.pickup
else if options.delivery and !options.pickup

View File

@@ -8,6 +8,7 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)->
for line_item in @line_items
line_item.variant.line_item = line_item
Variants.register line_item.variant
line_item.variant.extended_name = @extendedVariantName(line_item.variant)
orderChanged: =>
@unsaved()
@@ -63,8 +64,17 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http)->
@create_line_item(variant) unless exists
create_line_item: (variant)->
variant.extended_name = @extendedVariantName(variant)
variant.line_item =
variant: variant
quantity: null
max_quantity: null
@line_items.push variant.line_item
@line_items.push variant.line_item
extendedVariantName: (variant) =>
if variant.product_name == variant.name_to_display
variant.product_name
else
name = "#{variant.product_name} - #{variant.name_to_display}"
name += " (#{variant.options_text})" if variant.options_text
name

View File

@@ -1,11 +1,13 @@
Darkswarm.factory "OfnMap", (Enterprises, EnterpriseModal, visibleFilter)->
Darkswarm.factory "OfnMap", (Enterprises, EnterpriseModal, visibleFilter) ->
new class OfnMap
constructor: ->
@enterprises = (@extend(enterprise) for enterprise in visibleFilter(Enterprises.enterprises))
@enterprises = @enterprise_markers(Enterprises.enterprises)
enterprise_markers: (enterprises) ->
@extend(enterprise) for enterprise in visibleFilter(enterprises)
# Adding methods to each enterprise
extend: (enterprise)->
extend: (enterprise) ->
new class MapMarker
# We're whitelisting attributes because GMaps tries to crawl
# our data, and our data is recursive, so it breaks

View File

@@ -7,9 +7,9 @@
font-size: 120%
cursor: pointer
text-transform: uppercase
&.odd
&:nth-child(odd)
background-color: #ebf3fb
&.even
&:nth-child(even)
background-color: #ffffff
&:hover
background-color: #eaf0f5

View File

@@ -75,7 +75,7 @@ ordercycle
button.graph-button
z-index: 9999999
// z-index: 9999999
border: 1px solid transparent
padding: 0
margin: 0

View File

@@ -30,21 +30,20 @@
text-decoration: underline
span.margin-top
margin-top: 0.5rem
margin-top: 0.5rem
display: inline-block
// Generic text resize
@media all and (max-width: 640px)
@media all and (max-width: 640px)
&, & *
font-size: 0.875rem
font-size: 0.875rem
fat > div label
&, & *
font-size: 0.75rem
&, & *
font-size: 0.75rem
.active_table_row
// Inherits from active_table
border: 1px solid transparent
// Inherits from active_table
.active_table_row
border: 1px solid transparent
@include border-radius(0.5em)
// Foundation overrides
@@ -77,15 +76,15 @@
.active_table_row:last-child
border-bottom: 1px solid $disabled-bright
@include border-radius-mixed(0, 0, 0.5em, 0.5em)
//Open row sections
.fat > div
border-top: 1px solid #aaa
@media all and (max-width: 640px)
@media all and (max-width: 640px)
margin-top: 1em
ul, ol
ul, ol
font-size: 0.875rem
[class*="block-grid-"] > li
@@ -97,10 +96,10 @@
margin-top: 0.25rem
margin-bottom: 0.25rem
color: #777
p.trans-sentence
text-transform: capitalize
&.closed
&:hover, &:active, &:focus
.active_table_row.closed
@@ -113,7 +112,3 @@
&.open
.active_table_row:first-child
color: $dark-grey

View File

@@ -25,5 +25,6 @@ $disabled-v-dark: #808080
$med-grey: #666
$med-drk-grey: #444
$dark-grey: #333
$light-grey: #ddd
$black: #000

View File

@@ -2,6 +2,11 @@
@import branding
@import animations
.order-summary
background-color: #e1f0f5
padding: 1em
width: 100%
checkout
display: block
@@ -55,7 +60,6 @@ checkout
text-align: left
// Logic to swap out up / down accordion icons
//Foundation overrides
dd > a
@include csstrans

View File

@@ -1,6 +1,7 @@
@import branding
@import mixins
// Search page
#groups
background-color: $clr-brick-light
background-image: url("/assets/groups.svg")
@@ -8,35 +9,97 @@
background-repeat: no-repeat
padding-bottom: 20px
a > .group-name
&:hover, &:focus, &:active
text-decoration: underline
.groups-icons
text-align: right
a
font-size: 1.5em
.groups-header
border: 2px solid $clr-brick-light-bright
@include border-radius-mixed(0.5em, 0.5em, 0, 0)
margin: -1rem 0 1rem
padding: 1rem 0.9375rem
@media screen and (min-width: 640px)
border: 0 none
@include border-radius(0)
margin: 0
padding: 0
.group
padding-bottom: 40px
hr
border-bottom: 10px solid white
outline: 0
border-top: 0
margin: 0
padding-bottom: 0.5em
.row div
font-size: 110%
.row a
vertical-align: middle
.ofn-i_035-groups
font-size: 120%
vertical-align: middle
// Individual Page
#group-page
.group-logo, .group-header
text-align: center
.group-logo
padding-bottom: 1em
max-height: 200px
.group-name
border-bottom: 1px solid #ccc
@media screen and (min-width: 768px)
.group-logo, .group-header
text-align: left
.group-logo
max-height: 120px
float: left
padding-right: 1em
background-color: white
.group-hero
position: relative
padding: 0
border: 10px solid white
background: white
// Tabs
.tabs dd a // Mobile first
padding: 0.25rem 0.45rem 0rem
font-size: 0.75rem
border: none
margin-bottom: -2px
margin-right: 2px
text-transform: capitalize
@include avenir
@include border-radius(1em 0.25em 0 0)
@include gradient($disabled-light, $disabled-bright)
@media screen and (min-width: 768px)
.tabs dd a
padding: 0.5rem 1rem 0.25em
font-size: 0.875rem
@include border-radius(1.5em 0.25em 0 0)
@media screen and (min-width: 1024px)
.tabs dd a
padding: 0.75rem 1.5rem 0.5em
font-size: 1rem
@include border-radius(2em 0.25em 0 0)
.tabs dd.active a
@include gradient(white, white)
margin-bottom: -1px
border-top: 1px solid $light-grey
border-left: 1px solid $light-grey
border-right: 1px solid $light-grey
border-bottom: 0
.tabs-content
border-top: 1px solid $light-grey
border-left: 1px solid $light-grey
border-right: 1px solid $light-grey
border-bottom: 1px solid $light-grey
padding: 1.5em
h3.group-name
margin-top: 0.5em
margin-bottom: 0.15em
img.group-logo
max-width: 220px
max-height: 86px
float: right
padding-top: 10px
img.group-hero-img
background-color: black
width: 100%
height: inherit
max-height: 260px
min-height: 120px
overflow: hidden
// Producers tab
.producers
background-image: none
.active_table .active_table_node a.is_distributor, .active_table .active_table_node a.is_distributor i.ofn-i_059-producer
color: $clr-turquoise
// Hubs tab
.hubs
background-image: none
padding-top: 0
padding-bottom: 0

View File

@@ -10,12 +10,12 @@
height: 100%
width: 100%
img
// https://github.com/zurb/foundation/issues/112
// https://github.com/zurb/foundation/issues/112
img
max-width: none
height: auto
#pac-input
#pac-input
@include big-input(#888, #333, $clr-brick)
@include big-input-static
font-size: 1.5rem

View File

@@ -120,5 +120,21 @@
background-repeat: no-repeat
background-size: 100% auto
@mixin gradient($gradient-clr1, $gradient-clr2)
background: $gradient-clr1
// Old browsers
background: -moz-linear-gradient(top, $gradient-clr1 0%, $gradient-clr2 100%)
// FF3.6+
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, $gradient-clr1), color-stop(100%, $gradient-clr2))
// Chrome,Safari4+
background: -webkit-linear-gradient(top, $gradient-clr1 0%, $gradient-clr2 100%)
// Chrome10+,Safari5.1+
background: -o-linear-gradient(top, $gradient-clr1 0%, $gradient-clr2 100%)
// Opera 11.10+
background: -ms-linear-gradient(top, $gradient-clr1 0%, $gradient-clr2 100%)
// IE10+
background: linear-gradient(to bottom, $gradient-clr1 0%, $gradient-clr2 100%)
// W3C
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$gradient-clr1', endColorstr='$gradient-clr2',GradientType=0 )
// IE6-8

View File

@@ -15,7 +15,7 @@
font-size: 1rem
font-weight: 400
color: $disabled-dark
border-bottom: 1px solid $disabled-dark
border-bottom: 1px solid $light-grey
margin-top: 0.75rem
margin-bottom: 0.5rem
@@ -67,8 +67,8 @@
margin-bottom: 0.5rem
overflow-y: scroll
overflow-x: hidden
border-bottom: 1px solid #999
@include box-shadow(0 2px 2px -2px #999)
border-bottom: 1px solid $light-grey
@include box-shadow(0 2px 2px -2px $light-grey)
.enterprise-logo, img
float: left

View File

@@ -42,10 +42,22 @@
height: auto
top: 0px
// Shopping cart
#cart-detail
.cart-item-delete
a.delete
font-size: 1.125em
.item-thumb-image
display: none
@media screen and (min-width: 640px)
display: inline-block
float: left
padding-right: 0.5em
width: 36px
height: 36px
#edit-cart
button, .button

View File

@@ -16,27 +16,45 @@
@font-face
font-family: 'AvenirMed'
src: url("/AvenirLTStd-Medium.otf") format("opentype")
body
font-family: 'Open Sans', Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif
$font-helvetica: "Helvetica Neue", "HelveticaNeue", "Helvetica", Helvetica, Arial, sans-serif
a
color: $clr-brick
&:hover
&:hover, &:focus, &:active
text-decoration: none
color: $clr-brick-bright
.text-big
font-size: 1.5rem
font-weight: 300
small, .small
font-size: 0.75rem
.text-small
font-size: 0.875rem
margin-bottom: 0.5rem
font-family: $font-helvetica
&, & *
font-size: 0.875rem
.text-normal
font-weight: 400
font-family: $font-helvetica
.text-skinny
font-weight: 300
font-family: $font-helvetica
.word-wrap
word-wrap: break-word
.pre-wrap
white-space: pre-wrap
.pre-line
white-space: pre-line
.light
color: #999
@@ -81,6 +99,9 @@ ul.check-list
.light-grey
color: #666666
.pad
padding: 1em
.pad-top
padding-top: 1em

View File

@@ -56,6 +56,23 @@ table.social
table.order-summary
border-collapse: separate
border-spacing: 0px 10px
tbody tr td
padding-left: 5px
padding-right: 5px
thead tr th
background-color: #f2f2f2
border-bottom: 1px solid black
padding-left: 5px
padding-right: 5px
h4
margin-top: 15px
tfoot
tr:first-child td
border-top: 1px solid black
padding-top: 5px
tr td
padding-left: 5px
padding-right: 5px
.social .soc-btn
padding: 3px 7px
@@ -245,6 +262,10 @@ ul
tr td
padding: 15px
.pad
tr td
padding: 15px
.column-wrap
padding: 0!important
margin: 0 auto

View File

@@ -20,6 +20,17 @@ module Admin
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
end
end
def bulk_update
@enterprise_fee_set = EnterpriseFeeSet.new(params[:enterprise_fee_set])
if @enterprise_fee_set.save
@@ -59,12 +70,26 @@ module Admin
def load_data
@calculators = EnterpriseFee.calculators.sort_by(&:name)
@tax_categories = Spree::TaxCategory.order('is_default DESC, name ASC')
end
def collection
collection = EnterpriseFee.managed_by(spree_current_user).order('enterprise_id', 'fee_type', 'name')
collection = collection.for_enterprise(current_enterprise) if current_enterprise
collection
case action
when :for_order_cycle
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?
enterprises = OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user, order_cycle).visible_enterprises
return EnterpriseFee.for_enterprises(enterprises).order('enterprise_id', 'fee_type', 'name')
else
collection = EnterpriseFee.managed_by(spree_current_user).order('enterprise_id', 'fee_type', 'name')
collection = collection.for_enterprise(current_enterprise) if current_enterprise
collection
end
end
def collection_actions
[:index, :for_order_cycle]
end
def current_enterprise

View File

@@ -1,22 +1,50 @@
module Admin
class EnterpriseGroupsController < ResourceController
before_filter :load_data, except: :index
before_filter :load_object_data, only: [:new, :edit, :create, :update]
def index
@enterprise_groups = @enterprise_groups.managed_by(spree_current_user)
end
def move_up
@enterprise_group = EnterpriseGroup.find params[:enterprise_group_id]
@enterprise_group.move_higher
EnterpriseGroup.with_isolation_level_serializable do
@enterprise_group = EnterpriseGroup.find params[:enterprise_group_id]
@enterprise_group.move_higher
end
redirect_to main_app.admin_enterprise_groups_path
end
def move_down
@enterprise_group = EnterpriseGroup.find params[:enterprise_group_id]
@enterprise_group.move_lower
EnterpriseGroup.with_isolation_level_serializable do
@enterprise_group = EnterpriseGroup.find params[:enterprise_group_id]
@enterprise_group.move_lower
end
redirect_to main_app.admin_enterprise_groups_path
end
protected
def build_resource_with_address
enterprise_group = build_resource_without_address
enterprise_group.address = Spree::Address.new
enterprise_group.address.country = Spree::Country.find_by_id(Spree::Config[:default_country_id])
enterprise_group
end
alias_method_chain :build_resource, :address
private
def load_data
@countries = Spree::Country.order(:name)
@enterprises = Enterprise.activated
end
def load_object_data
@owner_email = @enterprise_group.andand.owner.andand.email || ""
end
def collection
EnterpriseGroup.by_position
end

View File

@@ -7,17 +7,19 @@ module Admin
before_filter :check_can_change_sells, only: :update
before_filter :check_can_change_bulk_sells, only: :bulk_update
before_filter :override_owner, only: :create
before_filter :override_sells, only: :create
before_filter :check_can_change_owner, only: :update
before_filter :check_can_change_bulk_owner, only: :bulk_update
before_filter :check_can_change_managers, only: :update
before_filter :strip_new_properties, only: [:create, :update]
before_filter :load_properties, only: [:edit, :update]
before_filter :setup_property, only: [:edit]
helper 'spree/products'
include ActionView::Helpers::TextHelper
include OrderCyclesHelper
def for_order_cycle
@collection = order_cycle_permitted_enterprises
end
def set_sells
enterprise = Enterprise.find_by_permalink(params[:id]) || Enterprise.find(params[:id])
attributes = { sells: params[:sells] }
@@ -45,17 +47,35 @@ module Admin
def bulk_update
@enterprise_set = EnterpriseSet.new(collection, params[:enterprise_set])
touched_enterprises = @enterprise_set.collection.select(&:changed?)
if @enterprise_set.save
flash[:success] = 'Enterprises updated successfully'
flash[:success] = "Enterprises updated successfully"
# 18-3-2015: It seems that the form for this action sometimes loads bogus values for
# the 'sells' field, and submitting that form results in a bunch of enterprises with
# values that have mysteriously changed. This statement is here to help debug that
# issue, and should be removed (along with its display in index.html.haml) when the
# issue has been resolved.
flash[:action] = "Updated #{pluralize(touched_enterprises.count, 'enterprise')}: #{touched_enterprises.map(&:name).join(', ')}"
redirect_to main_app.admin_enterprises_path
else
touched_ids = params[:enterprise_set][:collection_attributes].values.map { |v| v[:id].to_i }
@enterprise_set.collection.select! { |e| touched_ids.include? e.id }
@enterprise_set.collection.select! { |e| touched_enterprises.include? e }
flash[:error] = 'Update failed'
render :index
end
end
def for_order_cycle
respond_to do |format|
format.json do
render json: ActiveModel::ArraySerializer.new( @collection,
each_serializer: Api::Admin::ForOrderCycle::EnterpriseSerializer, spree_current_user: spree_current_user
).to_json
end
end
end
protected
def build_resource_with_address
@@ -83,10 +103,18 @@ module Admin
end
def collection
# TODO was ordered with is_distributor DESC as well, not sure why or how we want to sort this now
OpenFoodNetwork::Permissions.new(spree_current_user).
editable_enterprises.
order('is_primary_producer ASC, name')
case action
when :for_order_cycle
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
else
# TODO was ordered with is_distributor DESC as well, not sure why or how we want to sort this now
OpenFoodNetwork::Permissions.new(spree_current_user).
editable_enterprises.
order('is_primary_producer ASC, name')
end
end
def collection_actions
@@ -119,6 +147,14 @@ module Admin
params[:enterprise][:owner_id] = spree_current_user.id unless spree_current_user.admin?
end
def override_sells
unless spree_current_user.admin?
has_hub = spree_current_user.owned_enterprises.is_hub.any?
new_enterprise_is_producer = Enterprise.new(params[:enterprise]).is_primary_producer
params[:enterprise][:sells] = (has_hub && !new_enterprise_is_producer) ? 'any' : 'none'
end
end
def check_can_change_owner
unless ( spree_current_user == @enterprise.owner ) || spree_current_user.admin?
params[:enterprise].delete :owner_id
@@ -139,9 +175,27 @@ module Admin
end
end
def strip_new_properties
unless spree_current_user.admin? || params[:enterprise][:producer_properties_attributes].nil?
names = Spree::Property.pluck(:name)
params[:enterprise][:producer_properties_attributes].each do |key, property|
params[:enterprise][:producer_properties_attributes].delete key unless names.include? property[:property_name]
end
end
end
def load_properties
@properties = Spree::Property.pluck(:name)
end
def setup_property
@enterprise.producer_properties.build
end
# Overriding method on Spree's resource controller
def location_after_save
if params[:enterprise].key? :producer_properties_attributes
refered_from_edit = URI(request.referer).path == main_app.edit_admin_enterprise_path(@enterprise)
if params[:enterprise].key?(:producer_properties_attributes) && !refered_from_edit
main_app.admin_enterprises_path
else
main_app.edit_admin_enterprise_path(@enterprise)

View File

@@ -1,23 +1,32 @@
require 'open_food_network/permissions'
require 'open_food_network/order_cycle_permissions'
require 'open_food_network/order_cycle_form_applicator'
module Admin
class OrderCyclesController < ResourceController
include OrderCyclesHelper
before_filter :load_order_cycle_set, :only => :index
before_filter :load_data_for_index, :only => :index
before_filter :require_coordinator, only: :new
before_filter :remove_protected_attrs, only: [:update]
before_filter :remove_unauthorized_bulk_attrs, only: [:bulk_update]
around_filter :protect_invalid_destroy, only: :destroy
def show
respond_to do |format|
format.html
format.json
format.json do
render json: Api::Admin::OrderCycleSerializer.new(@order_cycle, current_user: spree_current_user).to_json
end
end
end
def new
respond_to do |format|
format.html
format.json
format.json do
render json: Api::Admin::OrderCycleSerializer.new(@order_cycle, current_user: spree_current_user).to_json
end
end
end
@@ -26,7 +35,7 @@ module Admin
respond_to do |format|
if @order_cycle.save
OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, order_cycle_permitted_enterprises).go!
OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, spree_current_user).go!
flash[:notice] = 'Your order cycle has been created.'
format.html { redirect_to admin_order_cycles_path }
@@ -43,7 +52,7 @@ module Admin
respond_to do |format|
if @order_cycle.update_attributes(params[:order_cycle])
OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, order_cycle_permitted_enterprises).go!
OpenFoodNetwork::OrderCycleFormApplicator.new(@order_cycle, spree_current_user).go!
flash[:notice] = 'Your order cycle has been updated.'
format.html { redirect_to admin_order_cycles_path }
@@ -72,18 +81,63 @@ module Admin
protected
def collection
ocs = OrderCycle.managed_by(spree_current_user)
def collection(show_more=false)
ocs = OrderCycle.accessible_by(spree_current_user)
ocs.undated +
ocs.soonest_closing +
ocs.soonest_opening +
ocs.most_recently_closed
(show_more ? ocs.closed : ocs.recently_closed)
end
private
def load_order_cycle_set
@order_cycle_set = OrderCycleSet.new :collection => collection
def load_data_for_index
@show_more = !!params[:show_more]
@order_cycle_set = OrderCycleSet.new :collection => collection(@show_more)
end
def require_coordinator
if params[:coordinator_id] && @order_cycle.coordinator = permitted_coordinating_enterprises_for(@order_cycle).find_by_id(params[:coordinator_id])
return
end
available_coordinators = permitted_coordinating_enterprises_for(@order_cycle).select(&:confirmed?)
case available_coordinators.count
when 0
flash[:error] = "None of your enterprises have permission to coordinate an order cycle"
redirect_to main_app.admin_order_cycles_path
when 1
@order_cycle.coordinator = available_coordinators.first
else
flash[:error] = "You don't have permission to create an order cycle coordinated by that enterprise" if params[:coordinator_id]
render :set_coordinator
end
end
def protect_invalid_destroy
begin
yield
rescue ActiveRecord::InvalidForeignKey
redirect_to main_app.admin_order_cycles_url
flash[:error] = "That order cycle has been selected by a customer and cannot be deleted. To prevent customers from accessing it, please close it instead."
end
end
def remove_protected_attrs
params[:order_cycle].delete :coordinator_id
unless Enterprise.managed_by(spree_current_user).include?(@order_cycle.coordinator)
params[:order_cycle].delete_if{ |k,v| [:name, :orders_open_at, :orders_close_at].include? k.to_sym }
end
end
def remove_unauthorized_bulk_attrs
params[:order_cycle_set][:collection_attributes].each do |i, hash|
order_cycle = OrderCycle.find(hash[:id])
unless Enterprise.managed_by(spree_current_user).include?(order_cycle.andand.coordinator)
params[:order_cycle_set][:collection_attributes].delete i
end
end
end
end
end

View File

@@ -2,27 +2,18 @@ require 'open_food_network/spree_api_key_loader'
module Admin
class VariantOverridesController < ResourceController
include OrderCyclesHelper
include OpenFoodNetwork::SpreeApiKeyLoader
before_filter :load_spree_api_key, only: :index
before_filter :load_data
def index
@hubs = order_cycle_hub_enterprises(without_validation: true)
# Used in JS to look up the name of the producer of each product
@producers = OpenFoodNetwork::Permissions.new(spree_current_user).
variant_override_producers
@hub_permissions = OpenFoodNetwork::Permissions.new(spree_current_user).
variant_override_enterprises_per_hub
@variant_overrides = VariantOverride.for_hubs(@hubs)
end
def bulk_update
collection_hash = Hash[params[:variant_overrides].each_with_index.map { |vo, i| [i, vo] }]
vo_set = VariantOverrideSet.new collection_attributes: collection_hash
vo_set = VariantOverrideSet.new @variant_overrides, collection_attributes: collection_hash
# Ensure we're authorised to update all variant overrides
vo_set.collection.each { |vo| authorize! :update, vo }
@@ -40,6 +31,21 @@ module Admin
end
private
def load_data
@hubs = OpenFoodNetwork::Permissions.new(spree_current_user).
variant_override_hubs.by_name
# Used in JS to look up the name of the producer of each product
@producers = OpenFoodNetwork::Permissions.new(spree_current_user).
variant_override_producers
@hub_permissions = OpenFoodNetwork::Permissions.new(spree_current_user).
variant_override_enterprises_per_hub
@variant_overrides = VariantOverride.for_hubs(@hubs)
end
def collection
end
end

View File

@@ -63,7 +63,9 @@ module Api
end
def override_sells
params[:enterprise][:sells] = 'unspecified'
has_hub = current_api_user.owned_enterprises.is_hub.any?
new_enterprise_is_producer = !!params[:enterprise][:is_primary_producer]
params[:enterprise][:sells] = (has_hub && !new_enterprise_is_producer) ? 'any' : 'unspecified'
end
def override_visible

View File

@@ -19,6 +19,7 @@ class CheckoutController < Spree::CheckoutController
def update
if @order.update_attributes(object_params)
check_order_for_phantom_fees
fire_event('spree.checkout.update')
while @order.state != "complete"
if @order.state == "payment"
@@ -58,6 +59,20 @@ class CheckoutController < Spree::CheckoutController
private
def check_order_for_phantom_fees
phantom_fees = @order.adjustments.joins('LEFT OUTER JOIN spree_line_items ON spree_line_items.id = spree_adjustments.source_id').
where("originator_type = 'EnterpriseFee' AND source_type = 'Spree::LineItem' AND spree_line_items.id IS NULL")
if phantom_fees.any?
Bugsnag.notify(RuntimeError.new("Phantom Fees"), {
phantom_fees: {
phantom_total: phantom_fees.sum(&:amount).to_s,
phantom_fees: phantom_fees.as_json
}
})
end
end
# Copied and modified from spree. Remove check for order state, since the state machine is
# progressed all the way in one go with the one page checkout.
def object_params
@@ -110,7 +125,7 @@ class CheckoutController < Spree::CheckoutController
last_used_bill_address, last_used_ship_address = find_last_used_addresses(@order.email)
preferred_bill_address, preferred_ship_address = spree_current_user.bill_address, spree_current_user.ship_address if spree_current_user.respond_to?(:bill_address) && spree_current_user.respond_to?(:ship_address)
@order.bill_address ||= preferred_bill_address || last_used_bill_address || Spree::Address.default
@order.ship_address ||= preferred_ship_address || last_used_ship_address || Spree::Address.default
@order.ship_address ||= preferred_ship_address || last_used_ship_address || Spree::Address.default
end
def after_payment

View File

@@ -5,4 +5,8 @@ class GroupsController < BaseController
def index
@groups = EnterpriseGroup.on_front_page.by_position
end
def show
@group = EnterpriseGroup.find params[:id]
end
end

View File

@@ -5,6 +5,7 @@ Spree::Admin::ProductsController.class_eval do
include OrderCyclesHelper
before_filter :load_form_data, :only => [:bulk_edit, :new, :create, :edit, :update]
before_filter :load_spree_api_key, :only => [:bulk_edit, :variant_overrides]
before_filter :strip_new_properties, only: [:create, :update]
alias_method :location_after_save_original, :location_after_save
@@ -95,4 +96,13 @@ Spree::Admin::ProductsController.class_eval do
@producers = OpenFoodNetwork::Permissions.new(spree_current_user).managed_product_enterprises.is_primary_producer.by_name
@taxons = Spree::Taxon.order(:name)
end
def strip_new_properties
unless spree_current_user.admin? || params[:product][:product_properties_attributes].nil?
names = Spree::Property.pluck(:name)
params[:product][:product_properties_attributes].each do |key, property|
params[:product][:product_properties_attributes].delete key unless names.include? property[:property_name]
end
end
end
end

View File

@@ -6,6 +6,7 @@ require 'open_food_network/order_grouper'
require 'open_food_network/customers_report'
require 'open_food_network/users_and_enterprises_report'
require 'open_food_network/order_cycle_management_report'
require 'open_food_network/sales_tax_report'
Spree::Admin::ReportsController.class_eval do
@@ -27,7 +28,8 @@ Spree::Admin::ReportsController.class_eval do
["Addresses", :addresses]
],
order_cycle_management: [
["Payment Methods Report", :payment_methods_report]
["Payment Methods Report", :payment_methods],
["Delivery Report", :delivery]
]
}
@@ -58,7 +60,6 @@ Spree::Admin::ReportsController.class_eval do
@report_types = REPORT_TYPES[:customers]
@report_type = params[:report_type]
@report = OpenFoodNetwork::CustomersReport.new spree_current_user, params
render_report(@report.header, @report.table, params[:csv], "customers_#{timestamp}.csv")
end
@@ -68,14 +69,13 @@ Spree::Admin::ReportsController.class_eval do
@report = OpenFoodNetwork::OrderCycleManagementReport.new spree_current_user, params
@search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q])
@orders = @search.result
render_report(@report.header, @report.table, params[:csv], "order_cycle_management_#{timestamp}.csv")
end
def orders_and_distributors
params[:q] = {} unless params[:q]
params[:q] ||= {}
if params[:q][:completed_at_gt].blank?
params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month
@@ -103,8 +103,38 @@ Spree::Admin::ReportsController.class_eval do
end
end
def sales_tax
params[:q] ||= {}
if params[:q][:completed_at_gt].blank?
params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month
else
params[:q][:completed_at_gt] = Time.zone.parse(params[:q][:completed_at_gt]).beginning_of_day rescue Time.zone.now.beginning_of_month
end
if params[:q] && !params[:q][:completed_at_lt].blank?
params[:q][:completed_at_lt] = Time.zone.parse(params[:q][:completed_at_lt]).end_of_day rescue ""
end
params[:q][:meta_sort] ||= "completed_at.desc"
@search = Spree::Order.complete.not_state(:canceled).managed_by(spree_current_user).search(params[:q])
orders = @search.result
@distributors = Enterprise.is_distributor.managed_by(spree_current_user)
@report = OpenFoodNetwork::SalesTaxReport.new orders
unless params[:csv]
render :html => @report
else
csv_string = CSV.generate do |csv|
csv << @report.header
@report.table.each { |row| csv << row }
end
send_data csv_string, :filename => "sales_tax.csv"
end
end
def bulk_coop
params[:q] = {} unless params[:q]
params[:q] ||= {}
if params[:q][:completed_at_gt].blank?
params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month
@@ -257,7 +287,7 @@ Spree::Admin::ReportsController.class_eval do
end
def payments
params[:q] = {} unless params[:q]
params[:q] ||= {}
if params[:q][:completed_at_gt].blank?
params[:q][:completed_at_gt] = Time.zone.now.beginning_of_month
@@ -495,18 +525,26 @@ Spree::Admin::ReportsController.class_eval do
table_items = @line_items
@include_blank = 'All'
header = ["Hub", "Customer", "Email", "Phone", "Producer", "Product", "Variant", "Amount", "Item (#{currency_symbol})", "Item + Fees (#{currency_symbol})", "Dist (#{currency_symbol})", "Ship (#{currency_symbol})", "Total (#{currency_symbol})", "Paid?",
"Shipping", "Delivery?", "Ship street", "Ship street 2", "Ship city", "Ship postcode", "Ship state", "Order notes"]
header = ["Hub", "Customer", "Email", "Phone", "Producer", "Product", "Variant",
"Amount", "Item (#{currency_symbol})", "Item + Fees (#{currency_symbol})", "Admin & Handling (#{currency_symbol})", "Ship (#{currency_symbol})", "Total (#{currency_symbol})", "Paid?",
"Shipping", "Delivery?",
"Ship Street", "Ship Street 2", "Ship City", "Ship Postcode", "Ship State",
"Comments", "SKU",
"Order Cycle", "Payment Method", "Customer Code", "Tags",
"Billing Street 1", "Billing Street 2", "Billing City", "Billing Postcode", "Billing State"
]
rsa = proc { |line_items| line_items.first.order.shipping_method.andand.require_ship_address }
columns = [ proc { |line_items| line_items.first.order.distributor.name },
columns = [
proc { |line_items| line_items.first.order.distributor.name },
proc { |line_items| line_items.first.order.bill_address.firstname + " " + line_items.first.order.bill_address.lastname },
proc { |line_items| line_items.first.order.email },
proc { |line_items| line_items.first.order.bill_address.phone },
proc { |line_items| line_items.first.variant.product.supplier.name },
proc { |line_items| line_items.first.variant.product.name },
proc { |line_items| line_items.first.variant.full_name },
proc { |line_items| line_items.sum { |li| li.quantity } },
proc { |line_items| line_items.sum { |li| li.amount } },
proc { |line_items| line_items.sum { |li| li.amount_with_adjustments } },
@@ -517,25 +555,40 @@ Spree::Admin::ReportsController.class_eval do
proc { |line_items| line_items.first.order.shipping_method.andand.name },
proc { |line_items| rsa.call(line_items) ? 'Y' : 'N' },
proc { |line_items| line_items.first.order.ship_address.andand.address1 if rsa.call(line_items) },
proc { |line_items| line_items.first.order.ship_address.andand.address2 if rsa.call(line_items) },
proc { |line_items| line_items.first.order.ship_address.andand.city if rsa.call(line_items) },
proc { |line_items| line_items.first.order.ship_address.andand.zipcode if rsa.call(line_items) },
proc { |line_items| line_items.first.order.ship_address.andand.state if rsa.call(line_items) },
proc { |line_items| line_items.first.order.special_instructions }]
proc { |line_items| "" },
proc { |line_items| line_items.first.variant.product.sku },
proc { |line_items| line_items.first.order.order_cycle.andand.name },
proc { |line_items| line_items.first.order.payments.first.andand.payment_method.andand.name },
proc { |line_items| line_items.first.order.user.andand.customer_of(line_items.first.order.distributor).andand.code },
proc { |line_items| "" },
proc { |line_items| line_items.first.order.bill_address.andand.address1 },
proc { |line_items| line_items.first.order.bill_address.andand.address2 },
proc { |line_items| line_items.first.order.bill_address.andand.city },
proc { |line_items| line_items.first.order.bill_address.andand.zipcode },
proc { |line_items| line_items.first.order.bill_address.andand.state } ]
rules = [ { group_by: proc { |line_item| line_item.order.distributor },
sort_by: proc { |distributor| distributor.name } },
{ group_by: proc { |line_item| line_item.order },
sort_by: proc { |order| order.bill_address.lastname + " " + order.bill_address.firstname },
summary_columns: [ proc { |line_items| line_items.first.order.distributor.name },
summary_columns: [
proc { |line_items| line_items.first.order.distributor.name },
proc { |line_items| line_items.first.order.bill_address.firstname + " " + line_items.first.order.bill_address.lastname },
proc { |line_items| "" },
proc { |line_items| "" },
proc { |line_items| "" },
proc { |line_items| "TOTAL" },
proc { |line_items| "" },
proc { |line_items| "" },
proc { |line_items| line_items.sum { |li| li.amount } },
proc { |line_items| line_items.sum { |li| li.amount_with_adjustments } },
@@ -546,13 +599,27 @@ Spree::Admin::ReportsController.class_eval do
proc { |line_items| "" },
proc { |line_items| "" },
proc { |line_items| "" },
proc { |line_items| "" },
proc { |line_items| "" },
proc { |line_items| "" },
proc { |line_items| "" },
proc { |line_items| "" } ] },
proc { |line_items| line_items.first.order.special_instructions } ,
proc { |line_items| "" },
proc { |line_items| line_items.first.order.order_cycle.andand.name },
proc { |line_items| line_items.first.order.payments.first.andand.payment_method.andand.name },
proc { |line_items| "" },
proc { |line_items| "" },
proc { |line_items| "" },
proc { |line_items| "" },
proc { |line_items| "" },
proc { |line_items| "" },
proc { |line_items| "" }
] },
{ group_by: proc { |line_item| line_item.variant.product },
sort_by: proc { |product| product.name } },
@@ -641,7 +708,8 @@ Spree::Admin::ReportsController.class_eval do
:products_and_inventory => {:name => "Products & Inventory", :description => ''},
:sales_total => { :name => "Sales Total", :description => "Sales Total For All Orders" },
:users_and_enterprises => { :name => "Users & Enterprises", :description => "Enterprise Ownership & Status" },
:order_cycle_management => {:name => "Order Cycle Management", :description => ''}
:order_cycle_management => {:name => "Order Cycle Management", :description => ''},
:sales_tax => { :name => "Sales Tax", :description => "Sales Tax For Orders" }
}
# Return only reports the user is authorized to view.
reports.select { |action| can? action, :report }

View File

@@ -20,15 +20,6 @@ Spree::Api::ProductsController.class_eval do
render_paged_products @products
end
def distributable
producers = OpenFoodNetwork::Permissions.new(current_api_user).
order_cycle_enterprises.is_primary_producer.by_name
@products = paged_products_for_producers producers
render_paged_products @products
end
def overridable
producers = OpenFoodNetwork::Permissions.new(current_api_user).
variant_override_producers.by_name

View File

@@ -62,6 +62,10 @@ module Admin
admin_inject_json_ams_array "ofn.admin", "variantOverrides", @variant_overrides, Api::Admin::VariantOverrideSerializer
end
def admin_inject_order_cycle_instance
render partial: "admin/json/injection_ams", locals: {ngModule: 'admin.order_cycles', name: 'ocInstance', json: "{coordinator_id: '#{@order_cycle.coordinator.id}'}"}
end
def admin_inject_spree_api_key
render partial: "admin/json/injection_ams", locals: {ngModule: 'ofn.admin', name: 'SpreeApiKey', json: "'#{@spree_api_key.to_s}'"}
end

View File

@@ -1,2 +1,23 @@
module GroupsHelper
def link_to_service(baseurl, name, html_options = {})
return if name.blank?
html_options = html_options.merge target: '_blank'
link_to ext_url(baseurl, name), html_options do
yield
end
end
def ext_url(prefix, url)
if url =~ /^https?:\/\//i
url
else
prefix + url
end
end
def strip_url(url)
url.andand.sub(/^https?:\/\//i, '')
end
end

View File

@@ -3,41 +3,32 @@ module OrderCyclesHelper
@current_order_cycle ||= current_order(false).andand.order_cycle
end
def order_cycle_permitted_enterprises
OpenFoodNetwork::Permissions.new(spree_current_user).order_cycle_enterprises
def permitted_enterprises_for(order_cycle)
OpenFoodNetwork::OrderCyclePermissions.new(spree_current_user, order_cycle).visible_enterprises
end
def order_cycle_producer_enterprises
order_cycle_permitted_enterprises.is_primary_producer.by_name
def permitted_producer_enterprises_for(order_cycle)
permitted_enterprises_for(order_cycle).is_primary_producer.by_name
end
def order_cycle_coordinating_enterprises
order_cycle_permitted_enterprises.is_distributor.by_name
def permitted_producer_enterprise_options_for(order_cycle)
validated_enterprise_options permitted_producer_enterprises_for(order_cycle), confirmed: true
end
def order_cycle_hub_enterprises(options={})
enterprises = order_cycle_permitted_enterprises.is_distributor.by_name
def permitted_coordinating_enterprises_for(order_cycle)
Enterprise.managed_by(spree_current_user).is_distributor.by_name
end
if options[:without_validation]
enterprises
else
enterprises.map do |e|
disabled_message = nil
if e.shipping_methods.empty? && e.payment_methods.available.empty?
disabled_message = 'no shipping or payment methods'
elsif e.shipping_methods.empty?
disabled_message = 'no shipping methods'
elsif e.payment_methods.available.empty?
disabled_message = 'no payment methods'
end
def permitted_coordinating_enterprise_options_for(order_cycle)
validated_enterprise_options permitted_coordinating_enterprises_for(order_cycle), confirmed: true
end
if disabled_message
["#{e.name} (#{disabled_message})", e.id, {disabled: true}]
else
[e.name, e.id]
end
end
end
def permitted_hub_enterprises_for(order_cycle)
permitted_enterprises_for(order_cycle).is_hub.by_name
end
def permitted_hub_enterprise_options_for(order_cycle)
validated_enterprise_options permitted_hub_enterprises_for(order_cycle), confirmed: true, shipping_and_payment_methods: true
end
def order_cycle_status_class(order_cycle)
@@ -68,8 +59,12 @@ module OrderCyclesHelper
OrderCycle.active.with_distributor(@distributor).present?
end
def order_cycles_simple_view
@order_cycles_simple_view ||= !OpenFoodNetwork::Permissions.new(spree_current_user).can_manage_complex_order_cycles?
def order_cycles_simple_index
@order_cycles_simple_index ||= !OpenFoodNetwork::Permissions.new(spree_current_user).can_manage_complex_order_cycles?
end
def order_cycles_simple_form
@order_cycles_simple_form ||= @order_cycle.coordinator.sells == 'own'
end
def order_cycles_enabled?
@@ -80,4 +75,36 @@ module OrderCyclesHelper
order_cycle.exchanges.to_enterprises(current_distributor).outgoing.first.pickup_time
end
def can_delete?(order_cycle)
Spree::Order.where(order_cycle_id: order_cycle).none?
end
def viewing_as_coordinator_of?(order_cycle)
Enterprise.managed_by(spree_current_user).include? order_cycle.coordinator
end
private
def validated_enterprise_options(enterprises, options={})
enterprises.map do |e|
disabled_message = nil
if options[:shipping_and_payment_methods] && (e.shipping_methods.empty? || e.payment_methods.available.empty?)
if e.shipping_methods.empty? && e.payment_methods.available.empty?
disabled_message = 'no shipping or payment methods'
elsif e.shipping_methods.empty?
disabled_message = 'no shipping methods'
elsif e.payment_methods.available.empty?
disabled_message = 'no payment methods'
end
elsif options[:confirmed] && !e.confirmed?
disabled_message = 'unconfirmed'
end
if disabled_message
["#{e.name} (#{disabled_message})", e.id, {disabled: true}]
else
[e.name, e.id]
end
end
end
end

View File

@@ -0,0 +1,6 @@
ConfirmOrderJob = Struct.new(:order_id) do
def perform
Spree::OrderMailer.confirm_email_for_customer(order_id).deliver
Spree::OrderMailer.confirm_email_for_shop(order_id).deliver
end
end

View File

@@ -0,0 +1,6 @@
ConfirmSignupJob = Struct.new(:user_id) do
def perform
user = Spree::User.find user_id
Spree::UserMailer.signup_confirmation(user).deliver
end
end

View File

@@ -0,0 +1,6 @@
WelcomeEnterpriseJob = Struct.new(:enterprise_id) do
def perform
enterprise = Enterprise.find enterprise_id
EnterpriseMailer.welcome(enterprise).deliver
end
end

10
app/models/customer.rb Normal file
View File

@@ -0,0 +1,10 @@
class Customer < ActiveRecord::Base
belongs_to :enterprise
belongs_to :user, :class_name => Spree.user_class
validates :code, presence: true, uniqueness: {scope: :enterprise_id}
validates :email, presence: true
validates :enterprise_id, presence: true
scope :of, ->(enterprise) { where(enterprise_id: enterprise) }
end

View File

@@ -15,6 +15,7 @@ class Enterprise < ActiveRecord::Base
has_and_belongs_to_many :groups, class_name: 'EnterpriseGroup'
has_many :producer_properties, foreign_key: 'producer_id'
has_many :properties, through: :producer_properties
has_many :supplied_products, :class_name => 'Spree::Product', :foreign_key => 'supplier_id', :dependent => :destroy
has_many :distributed_orders, :class_name => 'Spree::Order', :foreign_key => 'distributor_id'
belongs_to :address, :class_name => 'Spree::Address'
@@ -114,6 +115,9 @@ class Enterprise < ActiveRecord::Base
scope :with_distributed_products_outer,
joins('LEFT OUTER JOIN product_distributions ON product_distributions.distributor_id = enterprises.id').
joins('LEFT OUTER JOIN spree_products ON spree_products.id = product_distributions.product_id')
scope :with_order_cycles_as_supplier_outer,
joins("LEFT OUTER JOIN exchanges ON (exchanges.sender_id = enterprises.id AND exchanges.incoming = 't')").
joins('LEFT OUTER JOIN order_cycles ON (order_cycles.id = exchanges.order_cycle_id)')
scope :with_order_cycles_as_distributor_outer,
joins("LEFT OUTER JOIN exchanges ON (exchanges.receiver_id = enterprises.id AND exchanges.incoming = 'f')").
joins('LEFT OUTER JOIN order_cycles ON (order_cycles.id = exchanges.order_cycle_id)')
@@ -253,6 +257,10 @@ class Enterprise < ActiveRecord::Base
self.sells != "none"
end
def is_hub
self.sells == 'any'
end
# Simplify enterprise categories for frontend logic and icons, and maybe other things.
def category
# Make this crazy logic human readable so we can argue about it sanely.
@@ -340,7 +348,7 @@ class Enterprise < ActiveRecord::Base
end
def send_welcome_email
EnterpriseMailer.welcome(self).deliver
Delayed::Job.enqueue WelcomeEnterpriseJob.new(self.id)
end
def strip_url(url)
@@ -366,26 +374,30 @@ class Enterprise < ActiveRecord::Base
end
def relate_to_owners_enterprises
# When a new enterprise is created, we relate them to all enterprises owned by
# the same owner, in both directions. So all enterprises owned by the same owner
# will have permissions to every other one, in both directions.
# When a new producer is created, it grants permissions to all pre-existing hubs
# When a new hub is created,
# - it grants permissions to all pre-existing hubs
# - all producers grant permission to it
enterprises = owner.owned_enterprises.where('enterprises.id != ?', self)
enterprises.each do |enterprise|
# We grant permissions to all pre-existing hubs
hub_permissions = [:add_to_order_cycle]
hub_permissions << :create_variant_overrides if is_primary_producer
enterprises.is_hub.each do |enterprise|
EnterpriseRelationship.create!(parent: self,
child: enterprise,
permissions_list: [:add_to_order_cycle,
:manage_products,
:edit_profile,
:create_variant_overrides])
permissions_list: hub_permissions)
end
EnterpriseRelationship.create!(parent: enterprise,
child: self,
permissions_list: [:add_to_order_cycle,
:manage_products,
:edit_profile,
:create_variant_overrides])
# All pre-existing producers grant permission to new hubs
if is_hub
enterprises.is_primary_producer.each do |enterprise|
EnterpriseRelationship.create!(parent: enterprise,
child: self,
permissions_list: [:add_to_order_cycle,
:create_variant_overrides])
end
end
end

View File

@@ -1,5 +1,6 @@
class EnterpriseFee < ActiveRecord::Base
belongs_to :enterprise
belongs_to :tax_category, class_name: 'Spree::TaxCategory', foreign_key: 'tax_category_id'
has_and_belongs_to_many :order_cycles, join_table: 'coordinator_fees'
has_many :exchange_fees, dependent: :destroy
has_many :exchanges, through: :exchange_fees
@@ -8,7 +9,7 @@ class EnterpriseFee < ActiveRecord::Base
calculated_adjustments
attr_accessible :enterprise_id, :fee_type, :name, :calculator_type
attr_accessible :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type
FEE_TYPES = %w(packing transport admin sales fundraising)
PER_ORDER_CALCULATORS = ['Spree::Calculator::FlatRate', 'Spree::Calculator::FlexiRate']
@@ -19,6 +20,7 @@ class EnterpriseFee < ActiveRecord::Base
scope :for_enterprise, lambda { |enterprise| where(enterprise_id: enterprise) }
scope :for_enterprises, lambda { |enterprises| where(enterprise_id: enterprises) }
scope :managed_by, lambda { |user|
if user.has_spree_role?('admin')

View File

@@ -1,13 +1,28 @@
require 'open_food_network/locking'
class EnterpriseGroup < ActiveRecord::Base
acts_as_list
has_and_belongs_to_many :enterprises
belongs_to :owner, class_name: 'Spree::User', foreign_key: :owner_id, inverse_of: :owned_groups
belongs_to :address, :class_name => 'Spree::Address'
accepts_nested_attributes_for :address
validates :address, presence: true, associated: true
before_validation :set_undefined_address_fields
before_validation :set_unused_address_fields
after_find :unset_undefined_address_fields
after_save :unset_undefined_address_fields
validates :name, presence: true
validates :description, presence: true
attr_accessible :name, :description, :long_description, :on_front_page, :enterprise_ids
attr_accessible :owner_id
attr_accessible :logo, :promo_image
attr_accessible :address_attributes
attr_accessible :email, :website, :facebook, :instagram, :linkedin, :twitter
delegate :phone, :address1, :address2, :city, :zipcode, :state, :country, :to => :address
has_attached_file :logo,
styles: {medium: "100x100"},
@@ -28,4 +43,32 @@ class EnterpriseGroup < ActiveRecord::Base
scope :by_position, order('position ASC')
scope :on_front_page, where(on_front_page: true)
scope :managed_by, lambda { |user|
if user.has_spree_role?('admin')
scoped
else
where('owner_id = ?', user.id)
end
}
def set_unused_address_fields
address.firstname = address.lastname = 'unused' if address.present?
end
def set_undefined_address_fields
return unless address.present?
address.phone.present? || address.phone = 'undefined'
address.address1.present? || address.address1 = 'undefined'
address.city.present? || address.city = 'undefined'
address.zipcode.present? || address.zipcode = 'undefined'
end
def unset_undefined_address_fields
return unless address.present?
address.phone.sub!(/^undefined$/, '')
address.address1.sub!(/^undefined$/, '')
address.city.sub!(/^undefined$/, '')
address.zipcode.sub!(/^undefined$/, '')
end
end

View File

@@ -28,4 +28,8 @@ class EnterpriseRelationship < ActiveRecord::Base
def permissions_list=(perms)
perms.andand.each { |name| permissions.build name: name }
end
def has_permission?(name)
permissions.map(&:name).map(&:to_sym).include? name.to_sym
end
end

View File

@@ -22,6 +22,7 @@ class Exchange < ActiveRecord::Base
scope :to_enterprise, lambda { |enterprise| where(receiver_id: enterprise) }
scope :from_enterprises, lambda { |enterprises| where('exchanges.sender_id IN (?)', enterprises) }
scope :to_enterprises, lambda { |enterprises| where('exchanges.receiver_id IN (?)', enterprises) }
scope :involving, lambda { |enterprises| where('exchanges.receiver_id IN (?) OR exchanges.sender_id IN (?)', enterprises, enterprises).select('DISTINCT exchanges.*') }
scope :supplying_to, lambda { |distributor| where('exchanges.incoming OR exchanges.receiver_id = ?', distributor) }
scope :with_variant, lambda { |variant| joins(:exchange_variants).where('exchange_variants.variant_id = ?', variant) }
scope :with_any_variant, lambda { |variants| joins(:exchange_variants).where('exchange_variants.variant_id IN (?)', variants).select('DISTINCT exchanges.*') }

View File

@@ -19,7 +19,14 @@ class OrderCycle < ActiveRecord::Base
scope :undated, where(orders_open_at: nil, orders_close_at: nil)
scope :soonest_closing, lambda { active.order('order_cycles.orders_close_at ASC') }
# TODO This method returns all the closed orders. So maybe we can replace it with :recently_closed.
scope :most_recently_closed, lambda { closed.order('order_cycles.orders_close_at DESC') }
scope :recently_closed, -> {
closed.
where("order_cycles.orders_close_at >= ?", 31.days.ago).
order("order_cycles.orders_close_at DESC") }
scope :soonest_opening, lambda { upcoming.order('order_cycles.orders_open_at ASC') }
scope :distributing_product, lambda { |product|

View File

@@ -6,7 +6,9 @@ class AbilityDecorator
def initialize(user)
add_base_abilities user if is_new_user? user
add_enterprise_management_abilities user if can_manage_enterprises? user
add_group_management_abilities user if can_manage_groups? user
add_product_management_abilities user if can_manage_products? user
add_order_cycle_management_abilities user if can_manage_order_cycles? user
add_order_management_abilities user if can_manage_orders? user
add_relationship_management_abilities user if can_manage_relationships? user
end
@@ -21,12 +23,24 @@ class AbilityDecorator
user.enterprises.present?
end
# Users can manage a group if they have one.
def can_manage_groups?(user)
user.owned_groups.present?
end
# Users can manage products if they have an enterprise that is not a profile.
def can_manage_products?(user)
can_manage_enterprises?(user) &&
user.enterprises.any? { |e| e.category != :hub_profile && e.producer_profile_only != true }
end
# Users can manage order cycles if they manage a sells own/any enterprise
# OR if they manage a producer which is included in any order cycles
def can_manage_order_cycles?(user)
can_manage_orders?(user) ||
OrderCycle.accessible_by(user).any?
end
# Users can manage orders if they have a sells own/any enterprise.
def can_manage_orders?(user)
( user.enterprises.map(&:sells) & %w(own any) ).any?
@@ -41,6 +55,14 @@ class AbilityDecorator
can [:create], Enterprise
end
def add_group_management_abilities(user)
can [:admin, :index], :overview
can [:admin, :index], EnterpriseGroup
can [:read, :edit, :update], EnterpriseGroup do |group|
user.owned_groups.include? group
end
end
def add_enterprise_management_abilities(user)
# Spree performs authorize! on (:create, nil) when creating a new order from admin, and also (:search, nil)
# when searching for variants to add to the order
@@ -81,7 +103,7 @@ class AbilityDecorator
can [:admin, :index, :read, :update, :bulk_update], VariantOverride do |vo|
hub_auth = OpenFoodNetwork::Permissions.new(user).
order_cycle_enterprises.is_distributor.
variant_override_hubs.
include? vo.hub
producer_auth = OpenFoodNetwork::Permissions.new(user).
@@ -101,6 +123,17 @@ class AbilityDecorator
can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory], :report
end
def add_order_cycle_management_abilities(user)
can [:admin, :index, :read, :edit, :update], OrderCycle do |order_cycle|
OrderCycle.accessible_by(user).include? order_cycle
end
can [:bulk_update, :clone, :destroy], OrderCycle do |order_cycle|
user.enterprises.include? order_cycle.coordinator
end
can [:for_order_cycle], Enterprise
can [:for_order_cycle], EnterpriseFee
end
def add_order_management_abilities(user)
# Enterprise User can only access orders that they are a distributor for
can [:index, :create], Spree::Order
@@ -118,10 +151,6 @@ class AbilityDecorator
can [:admin, :index, :read, :create, :edit, :update, :fire], Spree::ReturnAuthorization
can [:create], OrderCycle
can [:admin, :index, :read, :edit, :update, :bulk_update, :clone], OrderCycle do |order_cycle|
user.enterprises.include? order_cycle.coordinator
end
can [:for_order_cycle], Enterprise
can [:admin, :index, :read, :create, :edit, :update], ExchangeVariant
can [:admin, :index, :read, :create, :edit, :update], Exchange
@@ -139,7 +168,7 @@ class AbilityDecorator
end
# Reports page
can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report
can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management], :report
end

View File

@@ -3,5 +3,17 @@ module Spree
has_one :metadata, class_name: 'AdjustmentMetadata', dependent: :destroy
scope :enterprise_fee, where(originator_type: 'EnterpriseFee')
scope :included_tax, where(originator_type: 'Spree::TaxRate', adjustable_type: 'Spree::LineItem')
attr_accessible :included_tax
def set_included_tax!(rate)
tax = amount - (amount / (1 + rate))
set_absolute_included_tax! tax
end
def set_absolute_included_tax!(tax)
update_attributes! included_tax: tax.round(2)
end
end
end

View File

@@ -1,9 +1,10 @@
Spree::AppConfiguration.class_eval do
# This file decorates the existing preferences file defined by Spree.
# It allows us to add our own global configuration variables, which
# we can allow to be modified in the UI by adding appropriate form
# elements to existing or new configuration pages.
# This file decorates the existing preferences file defined by Spree.
# It allows us to add our own global configuration variables, which
# we can allow to be modified in the UI by adding appropriate form
# elements to existing or new configuration pages.
# Tax Preferences
preference :products_require_tax_category, :boolean, default: false
end
# Tax Preferences
preference :products_require_tax_category, :boolean, default: false
preference :shipping_tax_rate, :decimal, default: 0
end

View File

@@ -26,6 +26,7 @@ Spree::LineItem.class_eval do
def price_with_adjustments
# EnterpriseFee#create_locked_adjustment applies adjustments on line items to their parent order,
# so line_item.adjustments returns an empty array
return 0 if quantity == 0
(price + order.adjustments.where(source_id: id).sum(&:amount) / quantity).round(2)
end

View File

@@ -0,0 +1,7 @@
Spree::Money.class_eval do
# return the currency symbol (on it's own) for the current default currency
def self.currency_symbol
Money.new(0, Spree::Config[:currency]).symbol
end
end

View File

@@ -68,7 +68,7 @@ Spree::Order.class_eval do
scope :with_payment_method_name, lambda { |payment_method_name|
joins(:payments => :payment_method).
where('spree_payment_methods.name = ?', payment_method_name).
where('spree_payment_methods.name IN (?)', payment_method_name).
select('DISTINCT spree_orders.*')
}
@@ -103,13 +103,17 @@ Spree::Order.class_eval do
def add_variant(variant, quantity = 1, max_quantity = nil, currency = nil)
line_items(:reload)
current_item = find_line_item_by_variant(variant)
if current_item
Bugsnag.notify(RuntimeError.new("Order populator weirdness"), {
# Notify bugsnag if we get line items with a quantity of zero
if quantity == 0
Bugsnag.notify(RuntimeError.new("Zero Quantity Line Item"), {
current_item: current_item.as_json,
line_items: line_items.map(&:id),
reloaded: line_items(:reload).map(&:id),
variant: variant.as_json
})
end
if current_item
current_item.quantity = quantity
current_item.max_quantity = max_quantity
@@ -198,16 +202,21 @@ Spree::Order.class_eval do
Spree::ShippingMethod.all_available(self, display_on)
end
def shipping_tax
adjustments(:reload).shipping.sum &:included_tax
end
def enterprise_fee_tax
adjustments(:reload).enterprise_fee.sum &:included_tax
end
def total_tax
(adjustments + price_adjustments).sum &:included_tax
end
# Overrride of Spree method, that allows us to send separate confirmation emails to user and shop owners
def deliver_order_confirmation_email
begin
Spree::OrderMailer.confirm_email_for_customer(self.id).deliver
Spree::OrderMailer.confirm_email_for_shop(self.id).deliver
rescue Exception => e
Bugsnag.notify(e)
logger.error("#{e.class.name}: #{e.message}")
logger.error(e.backtrace * "\n")
end
Delayed::Job.enqueue ConfirmOrderJob.new(id)
end

View File

@@ -0,0 +1,11 @@
module Spree
Shipment.class_eval do
def ensure_correct_adjustment_with_included_tax
ensure_correct_adjustment_without_included_tax
adjustment.set_included_tax! Config.shipping_tax_rate if Config.shipment_inc_vat
end
alias_method_chain :ensure_correct_adjustment, :included_tax
end
end

View File

@@ -0,0 +1,3 @@
Spree::ShippingCategory.class_eval do
attr_accessible :temperature_controlled
end

View File

@@ -0,0 +1,12 @@
Spree::TaxRate.class_eval do
def adjust_with_included_tax(order)
adjust_without_included_tax(order)
order.reload
(order.adjustments.tax + order.price_adjustments).each do |a|
a.set_absolute_included_tax! a.amount
end
end
alias_method_chain :adjust, :included_tax
end

View File

@@ -2,7 +2,9 @@ Spree.user_class.class_eval do
has_many :enterprise_roles, :dependent => :destroy
has_many :enterprises, through: :enterprise_roles
has_many :owned_enterprises, class_name: 'Enterprise', foreign_key: :owner_id, inverse_of: :owner
has_many :owned_groups, class_name: 'EnterpriseGroup', foreign_key: :owner_id, inverse_of: :owner
has_one :cart
has_many :customers
accepts_nested_attributes_for :enterprise_roles, :allow_destroy => true
@@ -11,6 +13,7 @@ Spree.user_class.class_eval do
validate :limit_owned_enterprises
def known_users
if admin?
Spree::User.scoped
@@ -29,14 +32,19 @@ Spree.user_class.class_eval do
end
end
def customer_of(enterprise)
customers.of(enterprise).first
end
def send_signup_confirmation
Spree::UserMailer.signup_confirmation(self).deliver
Delayed::Job.enqueue ConfirmSignupJob.new(id)
end
def can_own_more_enterprises?
owned_enterprises(:reload).size < enterprise_limit
end
private
def limit_owned_enterprises

View File

@@ -76,7 +76,9 @@ Spree::Variant.class_eval do
def full_name
return unit_to_display if display_name.blank?
display_name + " (" + unit_to_display + ")"
return display_name if display_name.downcase.include? unit_to_display.downcase
return unit_to_display if unit_to_display.downcase.include? display_name.downcase
"#{display_name} (#{unit_to_display})"
end
def name_to_display

View File

@@ -1,6 +1,6 @@
class VariantOverrideSet < ModelSet
def initialize(attributes={})
super(VariantOverride, VariantOverride.all, attributes, nil,
def initialize(collection, attributes={})
super(VariantOverride, collection, attributes, nil,
proc { |attrs| attrs['price'].blank? && attrs['count_on_hand'].blank? } )
end
end

View File

@@ -1,6 +0,0 @@
Deface::Override.new(:virtual_path => "spree/admin/shared/_configuration_menu",
:name => "add_enterprise_groups_to_admin_configurations_menu",
:insert_bottom => "[data-hook='admin_configurations_sidebar_menu']",
:text => "<li><%= link_to 'Enterprise Groups', main_app.admin_enterprise_groups_path %></li>",
:partial => 'enterprise_groups/admin_configurations_menu',
:original => '')

View File

@@ -1,5 +0,0 @@
Deface::Override.new(:virtual_path => "spree/layouts/admin",
:name => "add_enterprises_admin_tab",
:insert_bottom => "[data-hook='admin_tabs'], #admin_tabs[data-hook]",
:text => "<%= tab :enterprises, :url => main_app.admin_enterprises_path %>",
:original => '6999548b86c700f2cc5d4f9d297c94b3617fd981')

View File

@@ -1,5 +0,0 @@
Deface::Override.new(:virtual_path => "spree/layouts/admin",
:name => "add_order_cycles_admin_tab",
:insert_bottom => "[data-hook='admin_tabs'], #admin_tabs[data-hook]",
:text => "<%= tab :order_cycles, :url => main_app.admin_order_cycles_path %>",
:original => 'd4e321201ecb543e92192a031c8896a45dde3576')

View File

@@ -1,5 +0,0 @@
Deface::Override.new(:virtual_path => "spree/shared/_order_details",
:replace => "[data-hook='order_item_description']",
:partial => "spree/orders/order_item_description",
:name => "order_item_description",
:original => '1729abc5f441607b09cc0d44843a8dfd660ac5e0')

View File

@@ -0,0 +1,6 @@
/ replace_contents "td.property_name"
- if spree_current_user.admin?
= f.text_field :property_name, :class => 'autocomplete'
- else
= f.select :property_name, @properties, { :include_blank => true }, { class: 'select2 fullwidth' }

View File

@@ -0,0 +1,5 @@
/ insert_bottom "div[data-hook='admin_shipping_category_form_fields']"
%div.field.align-center{"data-hook" => "name"}
= f.label :temperature_controlled, t(:temperature_controlled)
= f.check_box :temperature_controlled

View File

@@ -0,0 +1,5 @@
/ insert_after "[data-hook='category_row'] td:first-child"
%td.align-center
= shipping_category.temperature_controlled ? 'Yes' : 'No'
%br/

View File

@@ -0,0 +1,4 @@
/ insert_after "[data-hook='categories_header'] th:first-child"
%th
Temperature Controlled

View File

@@ -0,0 +1,5 @@
/ insert_after "[data-hook='shipment_vat']"
.field.align-center{ "data-hook" => "shipping_tax_rate" }
= number_field_tag "preferences[shipping_tax_rate]", Spree::Config[:shipping_tax_rate].to_f, in: 0.0..1.0, step: 0.01
= label_tag nil, t(:shipping_tax_rate)

View File

@@ -0,0 +1,2 @@
/ insert_bottom "[data-hook='admin_tabs'], #admin_tabs[data-hook]"
= tab :enterprises, :url => main_app.admin_enterprises_path

View File

@@ -0,0 +1,2 @@
/ insert_bottom "[data-hook='admin_tabs'], #admin_tabs[data-hook]"
= tab :enterprise_groups, :url => main_app.admin_enterprise_groups_path, label: 'groups'

View File

@@ -0,0 +1,2 @@
/ insert_bottom "[data-hook='admin_tabs'], #admin_tabs[data-hook]"
= tab :order_cycles, :url => main_app.admin_order_cycles_path

View File

@@ -3,7 +3,7 @@ class EnterpriseFeePresenter
@controller, @enterprise_fee, @index = controller, enterprise_fee, index
end
delegate :id, :enterprise_id, :fee_type, :name, :calculator_type, :to => :enterprise_fee
delegate :id, :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type, :to => :enterprise_fee
def enterprise_fee
@enterprise_fee

View File

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

View File

@@ -0,0 +1,23 @@
class Api::Admin::EnterpriseFeeSerializer < ActiveModel::Serializer
attributes :id, :enterprise_id, :fee_type, :name, :tax_category_id, :calculator_type
attributes :enterprise_name, :calculator_description, :calculator_settings
def enterprise_name
object.enterprise.andand.name
end
def calculator_description
object.calculator.andand.description
end
def calculator_settings
result = nil
options[:controller].send(:with_format, :html) do
result = options[:controller].render_to_string :partial => 'admin/enterprise_fees/calculator_settings', :locals => {:enterprise_fee => object}
end
result.gsub('[0]', '[{{ $index }}]').gsub('_0_', '_{{ $index }}_')
end
end

View File

@@ -0,0 +1,17 @@
class Api::Admin::ExchangeSerializer < ActiveModel::Serializer
attributes :id, :sender_id, :receiver_id, :incoming, :variants, :pickup_time, :pickup_instructions
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)
else
permitted = OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object.order_cycle).
visible_variants_for_outgoing_exchanges_to(object.receiver)
end
Hash[ object.variants.merge(permitted).map { |v| [v.id, true] } ]
end
end

View File

@@ -0,0 +1,13 @@
class Api::Admin::ForOrderCycle::EnterpriseSerializer < ActiveModel::Serializer
attributes :id, :name, :managed, :supplied_products
def managed
Enterprise.managed_by(options[:spree_current_user]).include? object
end
def supplied_products
objects = object.supplied_products.not_deleted
serializer = Api::Admin::ForOrderCycle::SuppliedProductSerializer
ActiveModel::ArraySerializer.new(objects, each_serializer: serializer )
end
end

View File

@@ -0,0 +1,21 @@
class Api::Admin::ForOrderCycle::SuppliedProductSerializer < ActiveModel::Serializer
attributes :name, :supplier_name, :image_url, :master_id, :variants
def supplier_name
object.supplier.andand.name
end
def image_url
object.images.present? ? object.images.first.attachment.url(:mini) : nil
end
def master_id
object.master.id
end
def variants
object.variants.map do |variant|
{ id: variant.id, label: variant.full_name }
end
end
end

View File

@@ -0,0 +1,64 @@
class Api::Admin::OrderCycleSerializer < ActiveModel::Serializer
attributes :id, :name, :orders_open_at, :orders_close_at, :coordinator_id, :exchanges
attributes :editable_variants_for_incoming_exchanges, :editable_variants_for_outgoing_exchanges
attributes :visible_variants_for_outgoing_exchanges
attributes :viewing_as_coordinator
has_many :coordinator_fees, serializer: Api::IdSerializer
def orders_open_at
object.orders_open_at.to_s
end
def orders_close_at
object.orders_close_at.to_s
end
def viewing_as_coordinator
Enterprise.managed_by(options[:current_user]).include? object.coordinator
end
def exchanges
scoped_exchanges = OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object).visible_exchanges.order('id ASC')
ActiveModel::ArraySerializer.new(scoped_exchanges, {each_serializer: Api::Admin::ExchangeSerializer, current_user: options[:current_user] })
end
def editable_variants_for_incoming_exchanges
# For each enterprise that the current user is able to see in this order cycle,
# work out which variants should be editable within incoming exchanges from that enterprise
editable = {}
permissions = OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object)
enterprises = permissions.visible_enterprises
enterprises.each do |enterprise|
variants = permissions.editable_variants_for_incoming_exchanges_from(enterprise).pluck(:id)
editable[enterprise.id] = variants if variants.any?
end
editable
end
def editable_variants_for_outgoing_exchanges
# For each enterprise that the current user is able to see in this order cycle,
# work out which variants should be editable within incoming exchanges from that enterprise
editable = {}
permissions = OpenFoodNetwork::OrderCyclePermissions.new(options[:current_user], object)
enterprises = permissions.visible_enterprises
enterprises.each do |enterprise|
variants = permissions.editable_variants_for_outgoing_exchanges_to(enterprise).pluck(:id)
editable[enterprise.id] = variants if variants.any?
end
editable
end
def visible_variants_for_outgoing_exchanges
# For each enterprise that the current user is able to see in this order cycle,
# work out which variants should be visible within outgoing exchanges from that enterprise
visible = {}
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)
visible[enterprise.id] = variants if variants.any?
end
visible
end
end

View File

@@ -36,12 +36,10 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer
:long_description, :website, :instagram, :linkedin, :twitter,
:facebook, :is_primary_producer, :is_distributor, :phone, :visible,
:email, :hash, :logo, :promo_image, :path, :pickup, :delivery,
:icon, :icon_font, :producer_icon_font, :category
:icon, :icon_font, :producer_icon_font, :category, :producers, :hubs
has_many :distributed_taxons, key: :taxons, serializer: Api::IdSerializer
has_many :supplied_taxons, serializer: Api::IdSerializer
has_many :distributors, key: :hubs, serializer: Api::IdSerializer
has_many :suppliers, key: :producers, serializer: Api::IdSerializer
has_one :address, serializer: Api::AddressSerializer
@@ -73,6 +71,14 @@ class Api::CachedEnterpriseSerializer < ActiveModel::Serializer
enterprise_shop_path(object)
end
def producers
ActiveModel::ArraySerializer.new(object.suppliers.activated, {each_serializer: Api::IdSerializer})
end
def hubs
ActiveModel::ArraySerializer.new(object.distributors.activated, {each_serializer: Api::IdSerializer})
end
# Map svg icons.
def icon
icons = {

Some files were not shown because too many files have changed in this diff Show More