mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-16 04:24:23 +00:00
Merge remote-tracking branch 'origin/master' into uk/trial-length
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
angular.module("admin.customers").controller "customersCtrl", ($scope, CustomerResource, Columns, pendingChanges, shops) ->
|
||||
angular.module("admin.customers").controller "customersCtrl", ($scope, CustomerResource, TagsResource, $q, Columns, pendingChanges, shops) ->
|
||||
$scope.shop = {}
|
||||
$scope.shops = shops
|
||||
$scope.submitAll = pendingChanges.submitAll
|
||||
@@ -12,6 +12,16 @@ angular.module("admin.customers").controller "customersCtrl", ($scope, CustomerR
|
||||
if $scope.shop.id?
|
||||
$scope.customers = index {enterprise_id: $scope.shop.id}
|
||||
|
||||
$scope.findTags = (query) ->
|
||||
defer = $q.defer()
|
||||
params =
|
||||
enterprise_id: $scope.shop.id
|
||||
TagsResource.index params, (data) =>
|
||||
filtered = data.filter (tag) ->
|
||||
tag.text.toLowerCase().indexOf(query.toLowerCase()) != -1
|
||||
defer.resolve filtered
|
||||
defer.promise
|
||||
|
||||
$scope.add = (email) ->
|
||||
params =
|
||||
enterprise_id: $scope.shop.id
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
angular.module("admin.customers").factory 'TagsResource', ($resource) ->
|
||||
$resource('/admin/tags.json', {}, {
|
||||
'index':
|
||||
method: 'GET'
|
||||
isArray: true
|
||||
cache: true
|
||||
params:
|
||||
enterprise_id: '@enterprise_id'
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module("ofn.admin").directive "ofnTrackMaster", ["DirtyProducts", (DirtyProducts) ->
|
||||
angular.module("ofn.admin").directive "ofnTrackMaster", (DirtyProducts) ->
|
||||
require: "ngModel"
|
||||
link: (scope, element, attrs, ngModel) ->
|
||||
ngModel.$parsers.push (viewValue) ->
|
||||
@@ -6,4 +6,3 @@ angular.module("ofn.admin").directive "ofnTrackMaster", ["DirtyProducts", (Dirty
|
||||
DirtyProducts.addMasterProperty scope.product.id, scope.product.master.id, attrs.ofnTrackMaster, viewValue
|
||||
scope.displayDirtyProducts()
|
||||
viewValue
|
||||
]
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('ofn.admin').filter "translate", ->
|
||||
(key, options) ->
|
||||
t(key, options)
|
||||
|
||||
angular.module('ofn.admin').filter "t", ->
|
||||
(key, options) ->
|
||||
t(key, options)
|
||||
@@ -1,5 +1,5 @@
|
||||
angular.module('admin.orderCycles')
|
||||
.controller 'AdminEditOrderCycleCtrl', ($scope, $filter, $location, OrderCycle, Enterprise, EnterpriseFee, StatusMessage) ->
|
||||
.controller 'AdminEditOrderCycleCtrl', ($scope, $filter, $location, $window, OrderCycle, Enterprise, EnterpriseFee, StatusMessage) ->
|
||||
order_cycle_id = $location.absUrl().match(/\/admin\/order_cycles\/(\d+)/)[1]
|
||||
$scope.enterprises = Enterprise.index(order_cycle_id: order_cycle_id)
|
||||
$scope.supplier_enterprises = Enterprise.producer_enterprises
|
||||
@@ -12,6 +12,9 @@ angular.module('admin.orderCycles')
|
||||
|
||||
$scope.StatusMessage = StatusMessage
|
||||
|
||||
$scope.$watch 'order_cycle_form.$dirty', (newValue) ->
|
||||
StatusMessage.display 'notice', 'You have unsaved changes' if newValue
|
||||
|
||||
$scope.loaded = ->
|
||||
Enterprise.loaded && EnterpriseFee.loaded && OrderCycle.loaded
|
||||
|
||||
@@ -60,6 +63,7 @@ angular.module('admin.orderCycles')
|
||||
$scope.removeExchange = ($event, exchange) ->
|
||||
$event.preventDefault()
|
||||
OrderCycle.removeExchange(exchange)
|
||||
$scope.order_cycle_form.$dirty = true
|
||||
|
||||
$scope.addCoordinatorFee = ($event) ->
|
||||
$event.preventDefault()
|
||||
@@ -81,4 +85,9 @@ angular.module('admin.orderCycles')
|
||||
OrderCycle.removeDistributionOfVariant(variant_id)
|
||||
|
||||
$scope.submit = (destination) ->
|
||||
StatusMessage.display 'progress', "Saving..."
|
||||
OrderCycle.update(destination)
|
||||
$scope.order_cycle_form.$setPristine()
|
||||
|
||||
$scope.cancel = (destination) ->
|
||||
$window.location = destination
|
||||
|
||||
@@ -9,6 +9,9 @@ angular.module('admin.orderCycles').controller "AdminSimpleEditOrderCycleCtrl",
|
||||
$scope.order_cycle = OrderCycle.load $scope.orderCycleId(), (order_cycle) =>
|
||||
$scope.init()
|
||||
|
||||
$scope.$watch 'order_cycle_form.$dirty', (newValue) ->
|
||||
StatusMessage.display 'notice', 'You have unsaved changes' if newValue
|
||||
|
||||
$scope.loaded = ->
|
||||
Enterprise.loaded && EnterpriseFee.loaded && OrderCycle.loaded
|
||||
|
||||
@@ -35,5 +38,6 @@ angular.module('admin.orderCycles').controller "AdminSimpleEditOrderCycleCtrl",
|
||||
OrderCycle.removeCoordinatorFee(index)
|
||||
|
||||
$scope.submit = (destination) ->
|
||||
StatusMessage.display 'progress', "Saving..."
|
||||
OrderCycle.mirrorIncomingToOutgoingProducts()
|
||||
OrderCycle.update(destination)
|
||||
|
||||
@@ -1 +1 @@
|
||||
angular.module("admin.shippingMethods", ["ngTagsInput", 'admin.utils'])
|
||||
angular.module("admin.shippingMethods", ["ngTagsInput", 'admin.utils', 'templates'])
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
angular.module("admin.utils").directive "saveBar", (StatusMessage) ->
|
||||
restrict: "E"
|
||||
scope:
|
||||
save: "&"
|
||||
form: "="
|
||||
buttons: "="
|
||||
templateUrl: "admin/save_bar.html"
|
||||
link: (scope, element, attrs) ->
|
||||
scope.StatusMessage = StatusMessage
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
angular.module("admin.utils").directive "tagsWithTranslation", ($timeout) ->
|
||||
restrict: "E"
|
||||
template: "<tags-input ng-model='object[tagsAttr]'>"
|
||||
templateUrl: "admin/tags_input.html"
|
||||
scope:
|
||||
object: "="
|
||||
tagsAttr: "@?"
|
||||
tagListAttr: "@?"
|
||||
findTags: "&"
|
||||
link: (scope, element, attrs) ->
|
||||
$timeout ->
|
||||
scope.tagsAttr ||= "tags"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
angular.module("admin.utils").filter "translate", ->
|
||||
(key, options) ->
|
||||
t(key, options)
|
||||
|
||||
angular.module("admin.utils").filter "t", ->
|
||||
(key, options) ->
|
||||
t(key, options)
|
||||
@@ -7,6 +7,22 @@ Darkswarm.directive 'mapSearch', ($timeout)->
|
||||
link: (scope, elem, attrs, ctrl)->
|
||||
$timeout =>
|
||||
map = ctrl.getMap()
|
||||
|
||||
# Use OSM tiles server
|
||||
map.mapTypes.set 'OSM', new (google.maps.ImageMapType)(
|
||||
getTileUrl: (coord, zoom) ->
|
||||
# "Wrap" x (logitude) at 180th meridian properly
|
||||
# NB: Don't touch coord.x because coord param is by reference, and changing its x property breakes something in Google's lib
|
||||
tilesPerGlobe = 1 << zoom
|
||||
x = coord.x % tilesPerGlobe
|
||||
if x < 0
|
||||
x = tilesPerGlobe + x
|
||||
# Wrap y (latitude) in a like manner if you want to enable vertical infinite scroll
|
||||
'http://tile.openstreetmap.org/' + zoom + '/' + x + '/' + coord.y + '.png'
|
||||
tileSize: new (google.maps.Size)(256, 256)
|
||||
name: 'OpenStreetMap'
|
||||
maxZoom: 18)
|
||||
|
||||
input = (document.getElementById("pac-input"))
|
||||
map.controls[google.maps.ControlPosition.TOP_LEFT].push input
|
||||
searchBox = new google.maps.places.SearchBox((input))
|
||||
@@ -21,7 +37,7 @@ Darkswarm.directive 'mapSearch', ($timeout)->
|
||||
#map.setCenter place.geometry.location
|
||||
map.fitBounds place.geometry.viewport
|
||||
#map.fitBounds bounds
|
||||
|
||||
|
||||
# Bias the SearchBox results towards places that are within the bounds of the
|
||||
# current map's viewport.
|
||||
google.maps.event.addListener map, "bounds_changed", ->
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
Darkswarm.directive "ofnOnHand", ->
|
||||
restrict: 'A'
|
||||
require: "ngModel"
|
||||
|
||||
link: (scope, elem, attr, ngModel) ->
|
||||
# In cases where this field gets its value from the HTML element rather than the model,
|
||||
# initialise the model with the HTML value.
|
||||
if scope.$eval(attr.ngModel) == undefined
|
||||
ngModel.$setViewValue elem.val()
|
||||
|
||||
ngModel.$parsers.push (viewValue) ->
|
||||
on_hand = parseInt(attr.ofnOnHand)
|
||||
if parseInt(viewValue) > on_hand
|
||||
alert t('insufficient_stock', {on_hand: on_hand})
|
||||
viewValue = on_hand
|
||||
ngModel.$setViewValue viewValue
|
||||
ngModel.$render()
|
||||
|
||||
viewValue
|
||||
@@ -1,4 +1,4 @@
|
||||
Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, storage)->
|
||||
Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, $modal, $rootScope, storage)->
|
||||
# Handles syncing of current cart/order state to server
|
||||
new class Cart
|
||||
dirty: false
|
||||
@@ -28,15 +28,39 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, storage)->
|
||||
|
||||
update: =>
|
||||
@update_running = true
|
||||
|
||||
$http.post('/orders/populate', @data()).success (data, status)=>
|
||||
@saved()
|
||||
@update_running = false
|
||||
|
||||
@compareAndNotifyStockLevels data.stock_levels
|
||||
|
||||
@popQueue() if @update_enqueued
|
||||
|
||||
.error (response, status)=>
|
||||
@scheduleRetry(status)
|
||||
@update_running = false
|
||||
|
||||
compareAndNotifyStockLevels: (stockLevels) =>
|
||||
scope = $rootScope.$new(true)
|
||||
scope.variants = []
|
||||
|
||||
# TODO: These changes to quantity/max_quantity trigger another cart update, which
|
||||
# is unnecessary.
|
||||
|
||||
for li in @line_items_present()
|
||||
if stockLevels[li.variant.id]?
|
||||
li.variant.count_on_hand = stockLevels[li.variant.id].on_hand
|
||||
if li.quantity > li.variant.count_on_hand
|
||||
li.quantity = li.variant.count_on_hand
|
||||
scope.variants.push li.variant
|
||||
if li.variant.count_on_hand == 0 && li.max_quantity > li.variant.count_on_hand
|
||||
li.max_quantity = li.variant.count_on_hand
|
||||
scope.variants.push(li.variant) unless li.variant in scope.variants
|
||||
|
||||
if scope.variants.length > 0
|
||||
$modal.open(templateUrl: "out_of_stock.html", scope: scope, windowClass: 'out-of-stock-modal')
|
||||
|
||||
popQueue: =>
|
||||
@update_enqueued = false
|
||||
@scheduleUpdate()
|
||||
|
||||
@@ -10,9 +10,12 @@ Darkswarm.factory 'Checkout', (CurrentOrder, ShippingMethods, PaymentMethods, $h
|
||||
$http.put('/checkout', {order: @preprocess()}).success (data, status)=>
|
||||
Navigation.go data.path
|
||||
.error (response, status)=>
|
||||
Loading.clear()
|
||||
@errors = response.errors
|
||||
RailsFlashLoader.loadFlash(response.flash)
|
||||
if response.path
|
||||
Navigation.go response.path
|
||||
else
|
||||
Loading.clear()
|
||||
@errors = response.errors
|
||||
RailsFlashLoader.loadFlash(response.flash)
|
||||
|
||||
# Rails wants our Spree::Address data to be provided with _attributes
|
||||
preprocess: ->
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
Darkswarm.factory "MapConfiguration", ->
|
||||
new class MapConfiguration
|
||||
options:
|
||||
center:
|
||||
center:
|
||||
latitude: -37.4713077
|
||||
longitude: 144.7851531
|
||||
zoom: 12
|
||||
additional_options: {}
|
||||
#mapTypeId: 'satellite'
|
||||
additional_options:
|
||||
# mapTypeId: 'satellite'
|
||||
mapTypeId: 'OSM'
|
||||
mapTypeControl: false
|
||||
streetViewControl: false
|
||||
styles: [{"featureType":"landscape","stylers":[{"saturation":-100},{"lightness":65},{"visibility":"on"}]},{"featureType":"poi","stylers":[{"saturation":-100},{"lightness":51},{"visibility":"simplified"}]},{"featureType":"road.highway","stylers":[{"saturation":-100},{"visibility":"simplified"}]},{"featureType":"road.arterial","stylers":[{"saturation":-100},{"lightness":30},{"visibility":"on"}]},{"featureType":"road.local","stylers":[{"saturation":-100},{"lightness":40},{"visibility":"on"}]},{"featureType":"transit","stylers":[{"saturation":-100},{"visibility":"simplified"}]},{"featureType":"administrative.province","stylers":[{"visibility":"off"}]},{"featureType":"water","elementType":"labels","stylers":[{"visibility":"on"},{"lightness":-25},{"saturation":-100}]},{"featureType":"water","elementType":"geometry","stylers":[{"hue":"#ffff00"},{"lightness":-25},{"saturation":-97}]},{"featureType":"road","elementType": "labels.icon","stylers":[{"visibility":"off"}]}]
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#save-bar.animate-show{ ng: { show: 'form.$dirty || StatusMessage.active()' } }
|
||||
.twelve.columns.alpha
|
||||
%h5#status-message{ ng: { style: 'StatusMessage.statusMessage.style' } }
|
||||
{{ StatusMessage.statusMessage.text || " " }}
|
||||
.four.columns.omega.text-right
|
||||
%input.red{type: "button", value: "Save Changes", ng: { disabled: '!form.$dirty', click: "save()" } }
|
||||
.container
|
||||
.eight.columns.alpha
|
||||
%h5#status-message{ ng: { style: 'StatusMessage.statusMessage.style' } }
|
||||
{{ StatusMessage.statusMessage.text || " " }}
|
||||
.eight.columns.omega.text-right
|
||||
%input{"ng-repeat" => "button in buttons", type: "button", value: "{{button.text}}", ng: { class: "button.class", click: "button.action(button.param)" } }
|
||||
|
||||
8
app/assets/javascripts/templates/admin/tag.html.haml
Normal file
8
app/assets/javascripts/templates/admin/tag.html.haml
Normal file
@@ -0,0 +1,8 @@
|
||||
.tag-template
|
||||
%div
|
||||
%span.tag-with-rules{ ng: { if: "data.rules" }, "ofn-with-tip" => "{{ 'admin.tag_has_rules' | t:{num: data.rules} }}" }
|
||||
{{$getDisplayText()}}
|
||||
%span{ ng: { if: "!data.rules" } }
|
||||
{{$getDisplayText()}}
|
||||
%a.remove-button{ ng: {click: "$removeTag()"} }
|
||||
✖
|
||||
@@ -0,0 +1,11 @@
|
||||
.autocomplete-template
|
||||
%span.tag-with-rules{ ng: { if: "data.rules" } }
|
||||
{{$getDisplayText()}}
|
||||
%span.tag-with-rules{ ng: { if: "data.rules == 1" } }
|
||||
—
|
||||
= t 'admin.has_one_rule'
|
||||
%span.tag-with-rules{ ng: { if: "data.rules > 1" } }
|
||||
—
|
||||
= t 'admin.has_n_rules', { num: '{{data.rules}}' }
|
||||
%span{ ng: { if: "!data.rules" } }
|
||||
{{$getDisplayText()}}
|
||||
@@ -0,0 +1,7 @@
|
||||
%tags-input{ template: 'admin/tag.html', ng: { model: 'object[tagsAttr]' } }
|
||||
%auto-complete{source: "findTags({query: $query})",
|
||||
template: "admin/tag_autocomplete.html",
|
||||
"min-length" => "0",
|
||||
"load-on-focus" => "true",
|
||||
"load-on-empty" => "true",
|
||||
"max-results-to-show" => "32"}
|
||||
13
app/assets/javascripts/templates/out_of_stock.html.haml
Normal file
13
app/assets/javascripts/templates/out_of_stock.html.haml
Normal file
@@ -0,0 +1,13 @@
|
||||
%a.close-reveal-modal{"ng-click" => "$close()"}
|
||||
%i.ofn-i_009-close
|
||||
|
||||
%h3 Reduced stock available
|
||||
|
||||
%p While you've been shopping, the stock levels for one or more of the products in your cart have reduced. Here's what's changed:
|
||||
|
||||
%p{'ng-repeat' => "v in variants"}
|
||||
%em {{ v.name_to_display }} - {{ v.unit_to_display }}
|
||||
%span{'ng-if' => "v.count_on_hand == 0"}
|
||||
is now out of stock.
|
||||
%span{'ng-if' => "v.count_on_hand > 0"}
|
||||
now only has {{ v.count_on_hand }} remaining.
|
||||
@@ -0,0 +1,12 @@
|
||||
.small-5.medium-3.large-3.columns.text-right{"bo-if" => "!variant.product.group_buy"}
|
||||
|
||||
%input{type: :number,
|
||||
integer: true,
|
||||
value: nil,
|
||||
min: 0,
|
||||
placeholder: "0",
|
||||
"ofn-disable-scroll" => true,
|
||||
"ng-model" => "variant.line_item.quantity",
|
||||
"ofn-on-hand" => "{{variant.on_demand && 9999 || variant.count_on_hand }}",
|
||||
"ng-disabled" => "!variant.on_demand && variant.count_on_hand == 0",
|
||||
name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"}
|
||||
@@ -0,0 +1,23 @@
|
||||
.small-5.medium-3.large-3.columns.text-right{"bo-if" => "variant.product.group_buy"}
|
||||
%span.bulk-input-container
|
||||
%span.bulk-input
|
||||
%input.bulk.first{type: :number,
|
||||
value: nil,
|
||||
integer: true,
|
||||
min: 0,
|
||||
"ng-model" => "variant.line_item.quantity",
|
||||
placeholder: "{{'shop_variant_quantity_min' | t}}",
|
||||
"ofn-disable-scroll" => true,
|
||||
"ofn-on-hand" => "{{variant.on_demand && 9999 || variant.count_on_hand }}",
|
||||
name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"}
|
||||
%span.bulk-input
|
||||
%input.bulk.second{type: :number,
|
||||
"ng-disabled" => "!variant.line_item.quantity",
|
||||
integer: true,
|
||||
min: 0,
|
||||
"ng-model" => "variant.line_item.max_quantity",
|
||||
placeholder: "{{'shop_variant_quantity_max' | t}}",
|
||||
"ofn-disable-scroll" => true,
|
||||
min: "{{variant.line_item.quantity}}",
|
||||
name: "variant_attributes[{{variant.id}}][max_quantity]",
|
||||
id: "variants_{{variant.id}}_max"}
|
||||
@@ -1,61 +1,28 @@
|
||||
.variants.row
|
||||
.small-12.medium-4.large-4.columns.variant-name
|
||||
.table-cell
|
||||
.table-cell
|
||||
.inline {{ variant.name_to_display }}
|
||||
.bulk-buy.inline{"bo-if" => "variant.product.group_buy"}
|
||||
%i.ofn-i_056-bulk><
|
||||
%em><
|
||||
\ {{'bulk' | t}}
|
||||
|
||||
-# WITHOUT GROUP BUY
|
||||
.small-5.medium-3.large-3.columns.text-right{"bo-if" => "!variant.product.group_buy"}
|
||||
|
||||
%input{type: :number,
|
||||
integer: true,
|
||||
value: nil,
|
||||
min: 0,
|
||||
placeholder: "0",
|
||||
"ofn-disable-scroll" => true,
|
||||
"ng-model" => "variant.line_item.quantity",
|
||||
max: "{{variant.on_demand && 9999 || variant.count_on_hand }}",
|
||||
name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"}
|
||||
|
||||
%ng-include{src: "'partials/shop_variant_no_group_buy.html'"}
|
||||
%ng-include{src: "'partials/shop_variant_with_group_buy.html'"}
|
||||
|
||||
-# WITH GROUP BUY
|
||||
.small-5.medium-3.large-3.columns.text-right{"bo-if" => "variant.product.group_buy"}
|
||||
%span.bulk-input-container
|
||||
%span.bulk-input
|
||||
%input.bulk.first{type: :number,
|
||||
value: nil,
|
||||
integer: true,
|
||||
min: 0,
|
||||
"ng-model" => "variant.line_item.quantity",
|
||||
placeholder: "{{'shop_variant_quantity_min' | t}}",
|
||||
"ofn-disable-scroll" => true,
|
||||
max: "{{variant.on_demand && 9999 || variant.count_on_hand }}",
|
||||
name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"}
|
||||
%span.bulk-input
|
||||
%input.bulk.second{type: :number,
|
||||
"ng-disabled" => "!variant.line_item.quantity",
|
||||
integer: true,
|
||||
min: 0,
|
||||
"ng-model" => "variant.line_item.max_quantity",
|
||||
placeholder: "{{'shop_variant_quantity_max' | t}}",
|
||||
"ofn-disable-scroll" => true,
|
||||
max: "{{variant.on_demand && 9999 || variant.count_on_hand }}",
|
||||
name: "variant_attributes[{{variant.id}}][max_quantity]"}
|
||||
|
||||
.small-3.medium-1.large-1.columns.variant-unit
|
||||
.table-cell
|
||||
.table-cell
|
||||
%em {{ variant.unit_to_display }}
|
||||
|
||||
.small-4.medium-2.large-2.columns.variant-price
|
||||
.table-cell.price
|
||||
%i.ofn-i_009-close
|
||||
%i.ofn-i_009-close
|
||||
{{ variant.price_with_fees | localizeCurrency }}
|
||||
|
||||
-# Now in a template in app/assets/javascripts/templates !
|
||||
%price-breakdown{"price-breakdown" => "_", variant: "variant",
|
||||
%price-breakdown{"price-breakdown" => "_", variant: "variant",
|
||||
"price-breakdown-append-to-body" => "true",
|
||||
"price-breakdown-placement" => "left",
|
||||
"price-breakdown-animation" => true}
|
||||
@@ -63,4 +30,4 @@
|
||||
.small-12.medium-2.large-2.columns.total-price.text-right
|
||||
.table-cell
|
||||
%strong{"ng-class" => "{filled: variant.totalPrice()}"}
|
||||
{{ variant.totalPrice() | localizeCurrency }}
|
||||
{{ variant.totalPrice() | localizeCurrency }}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
#save-bar
|
||||
position: fixed
|
||||
width: 100%
|
||||
bottom: 0px
|
||||
padding: 8px 10px
|
||||
left: 0
|
||||
padding: 8px 8px
|
||||
font-weight: bold
|
||||
background-color: #fff
|
||||
background-color: #eff5fc
|
||||
color: #5498da
|
||||
h5
|
||||
color: #5498da
|
||||
input
|
||||
margin-right: 5px
|
||||
|
||||
3
app/assets/stylesheets/admin/customers.css.scss
Normal file
3
app/assets/stylesheets/admin/customers.css.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.tag-with-rules {
|
||||
color: black;
|
||||
}
|
||||
@@ -37,6 +37,7 @@ text-angular .ta-editor {
|
||||
|
||||
input.red {
|
||||
background-color: #DA5354;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
input.search {
|
||||
|
||||
@@ -11,20 +11,20 @@
|
||||
padding-bottom: 0em
|
||||
display: table
|
||||
line-height: 1.1
|
||||
// outline: 1px solid red
|
||||
// outline: 1px solid red
|
||||
|
||||
@media all and (max-width: 768px)
|
||||
font-size: 0.875rem
|
||||
@media all and (max-width: 768px)
|
||||
font-size: 0.875rem
|
||||
|
||||
@media all and (max-width: 640px)
|
||||
font-size: 0.75rem
|
||||
|
||||
@media all and (max-width: 640px)
|
||||
font-size: 0.75rem
|
||||
|
||||
.table-cell
|
||||
display: table-cell
|
||||
vertical-align: middle
|
||||
height: 37px
|
||||
|
||||
// ROW VARIANTS
|
||||
// ROW VARIANTS
|
||||
.row.variants
|
||||
margin-left: 0
|
||||
margin-right: 0
|
||||
@@ -35,7 +35,10 @@
|
||||
background-color: #f9f9f9
|
||||
&:hover, &:focus, &:active
|
||||
background-color: $clr-brick-ultra-light
|
||||
|
||||
|
||||
&.out-of-stock
|
||||
opacity: 0.2
|
||||
|
||||
// Variant name
|
||||
.variant-name
|
||||
padding-left: 7.9375rem
|
||||
@@ -52,7 +55,7 @@
|
||||
height: 27px
|
||||
|
||||
// Variant unit
|
||||
.variant-unit
|
||||
.variant-unit
|
||||
padding-left: 0rem
|
||||
padding-right: 0rem
|
||||
color: #888
|
||||
@@ -88,18 +91,18 @@
|
||||
margin-left: 0
|
||||
margin-right: 0
|
||||
background: #fff
|
||||
|
||||
|
||||
.columns
|
||||
padding-top: 1em
|
||||
padding-bottom: 1em
|
||||
line-height: 1
|
||||
|
||||
|
||||
@media all and (max-width: 768px)
|
||||
padding-top: 0.65rem
|
||||
padding-bottom: 0.65rem
|
||||
|
||||
.summary-header
|
||||
padding-left: 7.9375rem
|
||||
padding-left: 7.9375rem
|
||||
@media all and (max-width: 768px)
|
||||
padding-left: 4.9375rem
|
||||
@media all and (max-width: 640px)
|
||||
@@ -118,4 +121,3 @@
|
||||
color: $clr-brick
|
||||
i
|
||||
font-size: 0.8em
|
||||
|
||||
|
||||
@@ -32,6 +32,13 @@
|
||||
.debit
|
||||
color: $clr-brick
|
||||
|
||||
.invalid
|
||||
color: $ofn-grey
|
||||
.credit
|
||||
color: $ofn-grey
|
||||
.debit
|
||||
color: $ofn-grey
|
||||
|
||||
.distributor-balance.paid
|
||||
visibility: hidden
|
||||
|
||||
|
||||
@@ -22,7 +22,29 @@
|
||||
background: rgba(255,255,255,0.85)
|
||||
width: 50%
|
||||
margin-top: 1.2rem
|
||||
margin-left: 1rem
|
||||
@media all and (max-width: 768px)
|
||||
width: 80%
|
||||
&:active, &:focus, &.active
|
||||
background: rgba(255,255,255, 1)
|
||||
|
||||
.map-footer
|
||||
position: fixed
|
||||
z-index: 2
|
||||
width: 100%
|
||||
height: 23px
|
||||
left: 80px
|
||||
right: 0
|
||||
bottom: 6px
|
||||
margin: 0
|
||||
padding: 6px
|
||||
font-size: 14px
|
||||
font-weight: bold
|
||||
text-shadow: 2px 2px #aaa
|
||||
color: #fff
|
||||
|
||||
a, a:hover, a:active, a:focus
|
||||
color: #fff
|
||||
|
||||
@media all and (max-width: 1025px)
|
||||
left: 0px
|
||||
@@ -74,6 +74,9 @@ table.order-summary
|
||||
padding-left: 5px
|
||||
padding-right: 5px
|
||||
|
||||
.text-right
|
||||
text-align: right
|
||||
|
||||
.social .soc-btn
|
||||
padding: 3px 7px
|
||||
font-size: 12px
|
||||
|
||||
@@ -26,6 +26,23 @@ module Admin
|
||||
end
|
||||
end
|
||||
|
||||
# copy of Spree::Admin::ResourceController without flash notice
|
||||
def destroy
|
||||
invoke_callbacks(:destroy, :before)
|
||||
if @object.destroy
|
||||
invoke_callbacks(:destroy, :after)
|
||||
respond_with(@object) do |format|
|
||||
format.html { redirect_to location_after_destroy }
|
||||
format.js { render partial: "spree/admin/shared/destroy" }
|
||||
end
|
||||
else
|
||||
invoke_callbacks(:destroy, :fails)
|
||||
respond_with(@object) do |format|
|
||||
format.html { redirect_to location_after_destroy }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def collection
|
||||
|
||||
28
app/controllers/admin/tags_controller.rb
Normal file
28
app/controllers/admin/tags_controller.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
module Admin
|
||||
class TagsController < Spree::Admin::BaseController
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
serialiser = ActiveModel::ArraySerializer.new(tags_of_enterprise)
|
||||
render json: serialiser.to_json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def enterprise
|
||||
Enterprise.managed_by(spree_current_user).find_by_id(params[:enterprise_id])
|
||||
end
|
||||
|
||||
def tags_of_enterprise
|
||||
return [] unless enterprise
|
||||
tag_rule_map = enterprise.rules_per_tag
|
||||
tag_rule_map.keys.map do |tag|
|
||||
{ text: tag, rules: tag_rule_map[tag] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -151,8 +151,15 @@ class CheckoutController < Spree::CheckoutController
|
||||
|
||||
# Overriding Spree's methods
|
||||
def raise_insufficient_quantity
|
||||
flash[:error] = t(:spree_inventory_error_flash_for_insufficient_quantity)
|
||||
redirect_to main_app.shop_path
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to cart_path
|
||||
end
|
||||
|
||||
format.json do
|
||||
render json: {path: cart_path}, status: 400
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_to_paypal_express_form_if_needed
|
||||
|
||||
@@ -5,6 +5,8 @@ class EnterprisesController < BaseController
|
||||
|
||||
# These prepended filters are in the reverse order of execution
|
||||
prepend_before_filter :set_order_cycles, :require_distributor_chosen, :reset_order, only: :shop
|
||||
before_filter :check_stock_levels, only: :shop
|
||||
|
||||
before_filter :clean_permalink, only: :check_permalink
|
||||
|
||||
respond_to :js, only: :permalink_checker
|
||||
@@ -21,17 +23,24 @@ class EnterprisesController < BaseController
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def clean_permalink
|
||||
params[:permalink] = params[:permalink].parameterize
|
||||
end
|
||||
|
||||
def check_stock_levels
|
||||
if current_order(true).insufficient_stock_lines.present?
|
||||
redirect_to spree.cart_path
|
||||
end
|
||||
end
|
||||
|
||||
def reset_order
|
||||
distributor = Enterprise.is_distributor.find_by_permalink(params[:id]) || Enterprise.is_distributor.find(params[:id])
|
||||
order = current_order(true)
|
||||
|
||||
if order.distributor and order.distributor != distributor
|
||||
if order.distributor && order.distributor != distributor
|
||||
order.empty!
|
||||
order.set_order_cycle! nil
|
||||
end
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
module Spree
|
||||
module Admin
|
||||
GeneralSettingsController.class_eval do
|
||||
end
|
||||
|
||||
|
||||
module GeneralSettingsEditPreferences
|
||||
def edit
|
||||
super
|
||||
@preferences_general << :bugherd_api_key
|
||||
end
|
||||
end
|
||||
GeneralSettingsController.send(:prepend, GeneralSettingsEditPreferences)
|
||||
end
|
||||
end
|
||||
@@ -1,9 +1,9 @@
|
||||
require 'spree/core/controller_helpers/order_decorator'
|
||||
|
||||
Spree::OrdersController.class_eval do
|
||||
after_filter :populate_variant_attributes, :only => :populate
|
||||
before_filter :update_distribution, :only => :update
|
||||
before_filter :filter_order_params, :only => :update
|
||||
after_filter :populate_variant_attributes, only: :populate
|
||||
before_filter :update_distribution, only: :update
|
||||
before_filter :filter_order_params, only: :update
|
||||
|
||||
prepend_before_filter :require_order_cycle, only: :edit
|
||||
prepend_before_filter :require_distributor_chosen, only: :edit
|
||||
@@ -12,16 +12,58 @@ Spree::OrdersController.class_eval do
|
||||
include OrderCyclesHelper
|
||||
layout 'darkswarm'
|
||||
|
||||
|
||||
# Patching to redirect to shop if order is empty
|
||||
def edit
|
||||
@order = current_order(true)
|
||||
@insufficient_stock_lines = @order.insufficient_stock_lines
|
||||
|
||||
if @order.line_items.empty?
|
||||
redirect_to main_app.shop_path
|
||||
else
|
||||
associate_user
|
||||
|
||||
if @order.insufficient_stock_lines.present?
|
||||
flash[:error] = t(:spree_inventory_error_flash_for_insufficient_quantity)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def update
|
||||
@insufficient_stock_lines = []
|
||||
@order = current_order
|
||||
unless @order
|
||||
flash[:error] = t(:order_not_found)
|
||||
redirect_to root_path and return
|
||||
end
|
||||
|
||||
if @order.update_attributes(params[:order])
|
||||
@order.line_items = @order.line_items.select {|li| li.quantity > 0 }
|
||||
@order.restart_checkout_flow
|
||||
|
||||
render :edit and return unless apply_coupon_code
|
||||
|
||||
fire_event('spree.order.contents_changed')
|
||||
respond_with(@order) do |format|
|
||||
format.html do
|
||||
if params.has_key?(:checkout)
|
||||
@order.next_transition.run_callbacks if @order.cart?
|
||||
redirect_to checkout_state_path(@order.checkout_steps.first)
|
||||
else
|
||||
redirect_to cart_path
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
# Show order with original values, not newly entered ones
|
||||
@insufficient_stock_lines = @order.insufficient_stock_lines
|
||||
@order.line_items(true)
|
||||
respond_with(@order)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def populate
|
||||
# Without intervention, the Spree::Adjustment#update_adjustable callback is called many times
|
||||
# during cart population, for both taxation and enterprise fees. This operation triggers a
|
||||
@@ -30,19 +72,54 @@ Spree::OrdersController.class_eval do
|
||||
|
||||
Spree::Adjustment.without_callbacks do
|
||||
populator = Spree::OrderPopulator.new(current_order(true), current_currency)
|
||||
|
||||
if populator.populate(params.slice(:products, :variants, :quantity), true)
|
||||
fire_event('spree.cart.add')
|
||||
fire_event('spree.order.contents_changed')
|
||||
|
||||
current_order.cap_quantity_at_stock!
|
||||
current_order.update!
|
||||
|
||||
render json: true, status: 200
|
||||
variant_ids = variant_ids_in(populator.variants_h)
|
||||
|
||||
render json: {error: false, stock_levels: stock_levels(current_order, variant_ids)},
|
||||
status: 200
|
||||
|
||||
else
|
||||
render json: false, status: 402
|
||||
render json: {error: true}, status: 412
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Report the stock levels in the order for all variant ids requested
|
||||
def stock_levels(order, variant_ids)
|
||||
stock_levels = li_stock_levels(order)
|
||||
|
||||
li_variant_ids = stock_levels.keys
|
||||
(variant_ids - li_variant_ids).each do |variant_id|
|
||||
stock_levels[variant_id] = {quantity: 0, max_quantity: 0,
|
||||
on_hand: Spree::Variant.find(variant_id).on_hand}
|
||||
end
|
||||
|
||||
stock_levels
|
||||
end
|
||||
|
||||
def variant_ids_in(variants_h)
|
||||
variants_h.map { |v| v[:variant_id].to_i }
|
||||
end
|
||||
|
||||
def li_stock_levels(order)
|
||||
Hash[
|
||||
order.line_items.map do |li|
|
||||
[li.variant.id,
|
||||
{quantity: li.quantity,
|
||||
max_quantity: li.max_quantity,
|
||||
on_hand: wrap_json_infinity(li.variant.on_hand)}]
|
||||
end
|
||||
]
|
||||
end
|
||||
|
||||
def update_distribution
|
||||
@order = current_order(true)
|
||||
|
||||
@@ -121,4 +198,9 @@ Spree::OrdersController.class_eval do
|
||||
end
|
||||
end
|
||||
|
||||
# Rails to_json encodes Float::INFINITY as Infinity, which is not valid JSON
|
||||
# Return it as a large integer (max 32 bit signed int)
|
||||
def wrap_json_infinity(n)
|
||||
n == Float::INFINITY ? 2147483647 : n
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,19 +4,26 @@ class EnterpriseMailer < Spree::BaseMailer
|
||||
|
||||
def welcome(enterprise)
|
||||
@enterprise = enterprise
|
||||
mail(:to => enterprise.email, :from => from_address,
|
||||
:subject => "#{enterprise.name} is now on #{Spree::Config[:site_name]}")
|
||||
subject = t('enterprise_mailer.welcome.subject',
|
||||
enterprise: @enterprise.name,
|
||||
sitename: Spree::Config[:site_name])
|
||||
mail(:to => enterprise.email,
|
||||
:from => from_address,
|
||||
:subject => subject)
|
||||
end
|
||||
|
||||
def confirmation_instructions(record, token, opts={})
|
||||
def confirmation_instructions(record, token)
|
||||
@token = token
|
||||
find_enterprise(record)
|
||||
mail(subject: "Please confirm your email for #{@enterprise.name}",
|
||||
to: ( @enterprise.unconfirmed_email || @enterprise.email ),
|
||||
from: from_address)
|
||||
subject = t('enterprise_mailer.confirmation_instructions.subject',
|
||||
enterprise: @enterprise.name)
|
||||
mail(to: (@enterprise.unconfirmed_email || @enterprise.email),
|
||||
from: from_address,
|
||||
subject: subject)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_enterprise(enterprise)
|
||||
@enterprise = enterprise.is_a?(Enterprise) ? enterprise : Enterprise.find(enterprise)
|
||||
end
|
||||
|
||||
@@ -4,8 +4,11 @@ class ProducerMailer < Spree::BaseMailer
|
||||
@producer = producer
|
||||
@coordinator = order_cycle.coordinator
|
||||
@order_cycle = order_cycle
|
||||
@line_items = aggregated_line_items_from(@order_cycle, @producer)
|
||||
line_items = line_items_from(@order_cycle, @producer)
|
||||
@grouped_line_items = line_items.group_by(&:product_and_full_name)
|
||||
@receival_instructions = @order_cycle.receival_instructions_for @producer
|
||||
@total = total_from_line_items(line_items)
|
||||
@tax_total = tax_total_from_line_items(line_items)
|
||||
|
||||
subject = "[#{Spree::Config.site_name}] Order cycle report for #{producer.name}"
|
||||
|
||||
@@ -25,10 +28,6 @@ class ProducerMailer < Spree::BaseMailer
|
||||
line_items_from(order_cycle, producer).any?
|
||||
end
|
||||
|
||||
def aggregated_line_items_from(order_cycle, producer)
|
||||
aggregate_line_items line_items_from(order_cycle, producer)
|
||||
end
|
||||
|
||||
def line_items_from(order_cycle, producer)
|
||||
Spree::LineItem.
|
||||
joins(:order => :order_cycle, :variant => :product).
|
||||
@@ -37,16 +36,11 @@ class ProducerMailer < Spree::BaseMailer
|
||||
merge(Spree::Order.complete)
|
||||
end
|
||||
|
||||
def aggregate_line_items(line_items)
|
||||
# Arrange the items in a hash to group quantities
|
||||
line_items.inject({}) do |lis, li|
|
||||
if lis.key? li.variant
|
||||
lis[li.variant].quantity += li.quantity
|
||||
else
|
||||
lis[li.variant] = li
|
||||
end
|
||||
def total_from_line_items(line_items)
|
||||
Spree::Money.new line_items.sum(&:total)
|
||||
end
|
||||
|
||||
lis
|
||||
end
|
||||
def tax_total_from_line_items(line_items)
|
||||
Spree::Money.new line_items.sum(&:included_tax)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -351,6 +351,20 @@ class Enterprise < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
def rules_per_tag
|
||||
tag_rule_map = {}
|
||||
tag_rules.each do |rule|
|
||||
rule.preferred_customer_tags.split(",").each do |tag|
|
||||
if tag_rule_map[tag]
|
||||
tag_rule_map[tag] += 1
|
||||
else
|
||||
tag_rule_map[tag] = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
tag_rule_map
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def devise_mailer
|
||||
|
||||
@@ -101,11 +101,6 @@ class AbilityDecorator
|
||||
can [:print], Spree::Order do |order|
|
||||
order.user == user
|
||||
end
|
||||
|
||||
can [:create], Customer
|
||||
can [:destroy], Customer do |customer|
|
||||
user.enterprises.include? customer.enterprise
|
||||
end
|
||||
end
|
||||
|
||||
def add_product_management_abilities(user)
|
||||
@@ -221,7 +216,9 @@ class AbilityDecorator
|
||||
# Reports page
|
||||
can [:admin, :index, :customers, :group_buys, :bulk_coop, :sales_tax, :payments, :orders_and_distributors, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :xero_invoices], :report
|
||||
|
||||
can [:admin, :index, :update], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id)
|
||||
can [:create], Customer
|
||||
can [:admin, :index, :update, :destroy], Customer, enterprise_id: Enterprise.managed_by(user).pluck(:id)
|
||||
can [:admin, :index], :tag
|
||||
end
|
||||
|
||||
|
||||
|
||||
@@ -26,4 +26,6 @@ Spree::AppConfiguration.class_eval do
|
||||
# Monitoring
|
||||
preference :last_job_queue_heartbeat_at, :string, default: nil
|
||||
|
||||
# External services
|
||||
preference :bugherd_api_key, :string, default: nil
|
||||
end
|
||||
|
||||
@@ -43,6 +43,11 @@ Spree::LineItem.class_eval do
|
||||
where('spree_adjustments.id IS NULL')
|
||||
|
||||
|
||||
def cap_quantity_at_stock!
|
||||
update_attributes!(quantity: variant.on_hand) if quantity > variant.on_hand
|
||||
end
|
||||
|
||||
|
||||
def has_tax?
|
||||
adjustments.included_tax.any?
|
||||
end
|
||||
|
||||
@@ -99,7 +99,7 @@ Spree::Order.class_eval do
|
||||
def remove_variant(variant)
|
||||
line_items(:reload)
|
||||
current_item = find_line_item_by_variant(variant)
|
||||
current_item.destroy
|
||||
current_item.andand.destroy
|
||||
end
|
||||
|
||||
|
||||
@@ -144,6 +144,11 @@ Spree::Order.class_eval do
|
||||
current_item
|
||||
end
|
||||
|
||||
def cap_quantity_at_stock!
|
||||
line_items.each &:cap_quantity_at_stock!
|
||||
end
|
||||
|
||||
|
||||
def set_distributor!(distributor)
|
||||
self.distributor = distributor
|
||||
self.order_cycle = nil unless self.order_cycle.andand.has_distributor? distributor
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
require 'open_food_network/scope_variant_to_hub'
|
||||
|
||||
Spree::OrderPopulator.class_eval do
|
||||
attr_reader :variants_h
|
||||
|
||||
def populate(from_hash, overwrite = false)
|
||||
@distributor, @order_cycle = distributor_and_order_cycle
|
||||
# Refactor: We may not need this validation - we can't change distribution here, so
|
||||
@@ -11,8 +13,7 @@ Spree::OrderPopulator.class_eval do
|
||||
|
||||
if valid?
|
||||
@order.with_lock do
|
||||
variants = read_products_hash(from_hash) +
|
||||
read_variants_hash(from_hash)
|
||||
variants = read_variants from_hash
|
||||
|
||||
variants.each do |v|
|
||||
if varies_from_cart(v)
|
||||
@@ -31,6 +32,11 @@ Spree::OrderPopulator.class_eval do
|
||||
valid?
|
||||
end
|
||||
|
||||
def read_variants(data)
|
||||
@variants_h = read_products_hash(data) +
|
||||
read_variants_hash(data)
|
||||
end
|
||||
|
||||
def read_products_hash(data)
|
||||
(data[:products] || []).map do |product_id, variant_id|
|
||||
{variant_id: variant_id, quantity: data[:quantity]}
|
||||
@@ -49,17 +55,34 @@ Spree::OrderPopulator.class_eval do
|
||||
|
||||
def attempt_cart_add(variant_id, quantity, max_quantity = nil)
|
||||
quantity = quantity.to_i
|
||||
max_quantity = max_quantity.to_i if max_quantity
|
||||
variant = Spree::Variant.find(variant_id)
|
||||
OpenFoodNetwork::ScopeVariantToHub.new(@distributor).scope(variant)
|
||||
if quantity > 0
|
||||
if check_stock_levels(variant, quantity) &&
|
||||
check_order_cycle_provided_for(variant) &&
|
||||
check_variant_available_under_distribution(variant)
|
||||
@order.add_variant(variant, quantity, max_quantity, currency)
|
||||
if quantity > 0 &&
|
||||
check_order_cycle_provided_for(variant) &&
|
||||
check_variant_available_under_distribution(variant)
|
||||
|
||||
quantity_to_add, max_quantity_to_add = quantities_to_add(variant, quantity, max_quantity)
|
||||
|
||||
if quantity_to_add > 0
|
||||
@order.add_variant(variant, quantity_to_add, max_quantity_to_add, currency)
|
||||
else
|
||||
@order.remove_variant variant
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def quantities_to_add(variant, quantity, max_quantity)
|
||||
# If not enough stock is available, add as much as we can to the cart
|
||||
on_hand = variant.on_hand
|
||||
on_hand = [quantity, max_quantity].compact.max if Spree::Config.allow_backorders
|
||||
quantity_to_add = [quantity, on_hand].min
|
||||
max_quantity_to_add = max_quantity # max_quantity is not capped
|
||||
|
||||
[quantity_to_add, max_quantity_to_add]
|
||||
end
|
||||
|
||||
|
||||
def cart_remove(variant_id)
|
||||
variant = Spree::Variant.find(variant_id)
|
||||
@order.remove_variant(variant)
|
||||
|
||||
@@ -15,7 +15,6 @@ Spree.user_class.class_eval do
|
||||
accepts_nested_attributes_for :enterprise_roles, :allow_destroy => true
|
||||
|
||||
attr_accessible :enterprise_ids, :enterprise_roles_attributes, :enterprise_limit
|
||||
after_create :associate_customers
|
||||
after_create :send_signup_confirmation
|
||||
|
||||
validate :limit_owned_enterprises
|
||||
@@ -42,10 +41,6 @@ Spree.user_class.class_eval do
|
||||
customers.of(enterprise).first
|
||||
end
|
||||
|
||||
def associate_customers
|
||||
Customer.update_all({ user_id: id }, { user_id: nil, email: email })
|
||||
end
|
||||
|
||||
def send_signup_confirmation
|
||||
Delayed::Job.enqueue ConfirmSignupJob.new(id)
|
||||
end
|
||||
@@ -56,11 +51,16 @@ Spree.user_class.class_eval do
|
||||
|
||||
# Returns Enterprise IDs for distributors that the user has shopped at
|
||||
def enterprises_ordered_from
|
||||
orders.where(state: :complete).map(&:distributor_id).uniq
|
||||
enterprise_ids = orders.where(state: :complete).map(&:distributor_id).uniq
|
||||
# Exclude the accounts distributor
|
||||
if Spree::Config.accounts_distributor_id
|
||||
enterprise_ids = enterprise_ids.keep_if { |a| a != Spree::Config.accounts_distributor_id }
|
||||
end
|
||||
enterprise_ids
|
||||
end
|
||||
|
||||
# Returns orders and their associated payments for all distributors that have been ordered from
|
||||
def compelete_orders_by_distributor
|
||||
def complete_orders_by_distributor
|
||||
Enterprise
|
||||
.includes(distributed_orders: { payments: :payment_method })
|
||||
.where(enterprises: { id: enterprises_ordered_from },
|
||||
@@ -70,8 +70,8 @@ Spree.user_class.class_eval do
|
||||
|
||||
def orders_by_distributor
|
||||
# Remove uncompleted payments as these will not be reflected in order balance
|
||||
data_array = compelete_orders_by_distributor.to_a
|
||||
remove_uncompleted_payments(data_array)
|
||||
data_array = complete_orders_by_distributor.to_a
|
||||
remove_payments_in_checkout(data_array)
|
||||
data_array.sort! { |a, b| b.distributed_orders.length <=> a.distributed_orders.length }
|
||||
end
|
||||
|
||||
@@ -83,10 +83,10 @@ Spree.user_class.class_eval do
|
||||
end
|
||||
end
|
||||
|
||||
def remove_uncompleted_payments(enterprises)
|
||||
def remove_payments_in_checkout(enterprises)
|
||||
enterprises.each do |enterprise|
|
||||
enterprise.distributed_orders.each do |order|
|
||||
order.payments.keep_if { |payment| payment.state == "completed" }
|
||||
order.payments.keep_if { |payment| payment.state != "checkout" }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,6 +6,10 @@ class Api::Admin::CustomerSerializer < ActiveModel::Serializer
|
||||
end
|
||||
|
||||
def tags
|
||||
object.tag_list.map{ |t| { text: t } }
|
||||
tag_rule_map = object.enterprise.rules_per_tag
|
||||
object.tag_list.map do |tag|
|
||||
{ text: tag, rules: tag_rule_map[tag] }
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module Api
|
||||
class PaymentSerializer < ActiveModel::Serializer
|
||||
attributes :amount, :updated_at, :payment_method
|
||||
attributes :amount, :updated_at, :payment_method, :state
|
||||
def payment_method
|
||||
object.payment_method.name
|
||||
end
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
%input{ :type => 'text', :name => 'code', :id => 'code', 'ng-model' => 'customer.code', 'obj-for-update' => "customer", "attr-for-update" => "code" }
|
||||
%td.tags{ 'ng-show' => 'columns.tags.visible' }
|
||||
.tag_watcher{ 'obj-for-update' => "customer", "attr-for-update" => "tag_list"}
|
||||
%tags_with_translation{ object: 'customer' }
|
||||
%tags_with_translation{ object: 'customer', 'find-tags' => 'findTags(query)' }
|
||||
%td.actions
|
||||
%a{ 'ng-click' => "deleteCustomer(customer)", :class => "delete-customer icon-trash no-text" }
|
||||
%input{ :type => "button", 'value' => 'Update', 'ng-click' => 'submitAll()' }
|
||||
|
||||
@@ -52,14 +52,10 @@
|
||||
.actions
|
||||
- if @order_cycle.new_record?
|
||||
= f.submit 'Create', 'ng-click' => "submit('#{main_app.admin_order_cycles_path}')", 'ng-disabled' => '!loaded()'
|
||||
- else
|
||||
= f.submit 'Update', 'ng-click' => "submit(null)", 'ng-disabled' => '!loaded()'
|
||||
= f.submit 'Update and Close', 'ng-click' => "submit('#{main_app.admin_order_cycles_path}')", 'ng-disabled' => '!loaded()'
|
||||
|
||||
%span{'ng-show' => 'loaded()'}
|
||||
or
|
||||
= link_to 'Cancel', main_app.admin_order_cycles_path
|
||||
%span{'ng-hide' => 'loaded()'} Loading...
|
||||
= render 'spree/admin/shared/status_message'
|
||||
|
||||
|
||||
- unless Rails.env.production?
|
||||
|
||||
@@ -23,12 +23,7 @@
|
||||
.actions
|
||||
- if @order_cycle.new_record?
|
||||
= f.submit 'Create', 'ng-click' => "submit('#{main_app.admin_order_cycles_path}')", 'ng-disabled' => '!loaded()'
|
||||
- else
|
||||
= f.submit 'Update', 'ng-click' => "submit(null)", 'ng-disabled' => '!loaded()'
|
||||
= f.submit 'Update and Close', 'ng-click' => "submit('#{main_app.admin_order_cycles_path}')", 'ng-disabled' => '!loaded()'
|
||||
|
||||
%span{'ng-show' => 'loaded()'}
|
||||
or
|
||||
= link_to 'Cancel', main_app.admin_order_cycles_path
|
||||
%span{'ng-hide' => 'loaded()'} Loading...
|
||||
= render 'spree/admin/shared/status_message'
|
||||
|
||||
@@ -27,7 +27,10 @@
|
||||
|
||||
|
||||
- ng_controller = order_cycles_simple_form ? 'AdminSimpleEditOrderCycleCtrl' : 'AdminEditOrderCycleCtrl'
|
||||
= form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.orderCycles', 'ng-controller' => ng_controller} do |f|
|
||||
= form_for [main_app, :admin, @order_cycle], :url => '', :html => {:class => 'ng order_cycle', 'ng-app' => 'admin.orderCycles', 'ng-controller' => ng_controller, name: 'order_cycle_form'} do |f|
|
||||
|
||||
%save-bar{ buttons: "[{ text: 'Update', action: submit, param: null, class: 'red' }, { text: 'Update and Close', action: submit, param: '#{main_app.admin_order_cycles_path}', class: 'red' }, { text: 'Cancel', action: cancel, param: '#{main_app.admin_order_cycles_path}', class: '' }]", form: "order_cycle_form" }
|
||||
|
||||
- if order_cycles_simple_form
|
||||
= render 'simple_form', f: f
|
||||
- else
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
%form{ name: 'variant_overrides_form', ng: { show: "views.inventory.visible" } }
|
||||
%save-bar{ save: "update()", form: "variant_overrides_form" }
|
||||
%save-bar{ form: "variant_overrides_form", buttons: "[{ text: 'Save Changes', action: update, class: 'red' }]" }
|
||||
%table.index.bulk#variant-overrides
|
||||
%col.producer{ width: "20%", ng: { show: 'columns.producer.visible' } }
|
||||
%col.product{ width: "20%", ng: { show: 'columns.product.visible' } }
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
.map-container
|
||||
%map{"ng-if" => "(active(\'\') && (mapShowed = true)) || mapShowed"}
|
||||
%google-map{options: "map.additional_options", center: "map.center", zoom: "map.zoom", styles: "map.styles", draggable: "true"}
|
||||
%map-search
|
||||
%markers{models: "mapMarkers", fit: "true",
|
||||
coords: "'self'", icon: "'icon'", click: "'reveal'"}
|
||||
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
- if Rails.env.staging? or Rails.env.production?
|
||||
- if (Rails.env.staging? || Rails.env.production?) && Spree::Config.bugherd_api_key.present?
|
||||
:javascript
|
||||
(function (d, t) {
|
||||
var bh = d.createElement(t), s = d.getElementsByTagName(t)[0];
|
||||
bh.type = 'text/javascript';
|
||||
bh.src = '//www.bugherd.com/sidebarv2.js?apikey=4ftxjbgwx7y6ssykayr04w';
|
||||
bh.src = '//www.bugherd.com/sidebarv2.js?apikey=#{Spree::Config.bugherd_api_key}';
|
||||
s.parentNode.insertBefore(bh, s);
|
||||
})(document, 'script');
|
||||
|
||||
|
||||
-#- elsif Rails.env.production?
|
||||
-#:javascript
|
||||
-#(function (d, t) {
|
||||
-#var bh = d.createElement(t), s = d.getElementsByTagName(t)[0];
|
||||
-#bh.type = 'text/javascript';
|
||||
-#bh.src = '//www.bugherd.com/sidebarv2.js?apikey=xro3uv55objies58o2wrua';
|
||||
-#s.parentNode.insertBefore(bh, s);
|
||||
-#})(document, 'script');
|
||||
|
||||
@@ -9,3 +9,6 @@
|
||||
%map-search
|
||||
%markers{models: "OfnMap.enterprises", fit: "true",
|
||||
coords: "'self'", icon: "'icon'", click: "'reveal'"}
|
||||
|
||||
.map-footer
|
||||
%a{:href => "http://www.openstreetmap.org/copyright"} © OpenStreetMap contributors
|
||||
|
||||
74
app/views/producer_mailer/order_cycle_report.html.haml
Normal file
74
app/views/producer_mailer/order_cycle_report.html.haml
Normal file
@@ -0,0 +1,74 @@
|
||||
%p
|
||||
= t :producer_mail_greeting
|
||||
#{" " + @producer.name},
|
||||
%p
|
||||
= t :producer_mail_text_before
|
||||
- if @receival_instructions
|
||||
%p
|
||||
%b
|
||||
=t :producer_mail_delivery_instructions
|
||||
= @receival_instructions
|
||||
%p
|
||||
= t :producer_mail_order_text
|
||||
%table.order-summary
|
||||
%thead
|
||||
%tr
|
||||
%th
|
||||
= t :sku
|
||||
%th
|
||||
= t :supplier
|
||||
%th
|
||||
= t :product
|
||||
%th.text-right
|
||||
= t :quantity
|
||||
%th.text-right
|
||||
= t :price
|
||||
%th.text-right
|
||||
= t :subtotal
|
||||
%th.text-right
|
||||
= t :included_tax
|
||||
%tbody
|
||||
- @grouped_line_items.each_pair do |product_and_full_name, line_items|
|
||||
%tr
|
||||
%td
|
||||
#{line_items.first.variant.sku}
|
||||
%td
|
||||
#{raw(line_items.first.product.supplier.name)}
|
||||
%td
|
||||
#{raw(product_and_full_name)}
|
||||
%td.text-right
|
||||
#{line_items.sum(&:quantity)}
|
||||
%td.text-right
|
||||
#{line_items.first.single_money}
|
||||
%td.text-right
|
||||
#{Spree::Money.new(line_items.sum(&:total), currency: line_items.first.currency) }
|
||||
%td.tax.text-right
|
||||
#{Spree::Money.new(line_items.sum(&:included_tax), currency: line_items.first.currency) }
|
||||
%tr.total-row
|
||||
%td
|
||||
%td
|
||||
%td
|
||||
%td
|
||||
%td
|
||||
%td.text-right
|
||||
#{@total}
|
||||
%td.text-right
|
||||
#{@tax_total}
|
||||
%p
|
||||
= t :producer_mail_text_after
|
||||
%p
|
||||
#{t(:producer_mail_signoff)},
|
||||
%em
|
||||
%p
|
||||
#{@coordinator.name}
|
||||
%p
|
||||
%br
|
||||
#{@coordinator.address.address1}
|
||||
%br
|
||||
#{@coordinator.address.city}
|
||||
%br
|
||||
#{@coordinator.address.zipcode}
|
||||
%p
|
||||
#{@coordinator.phone}
|
||||
%p
|
||||
#{@coordinator.email}
|
||||
@@ -1,21 +1,26 @@
|
||||
Dear #{@producer.name},
|
||||
#{t :producer_mail_greeting} #{@producer.name},
|
||||
\
|
||||
We now have all the consumer orders for the next food drop.
|
||||
= t :producer_mail_text_before
|
||||
\
|
||||
- if @receival_instructions
|
||||
Stock pickup/delivery instructions:
|
||||
= t :producer_mail_delivery_instructions
|
||||
= @receival_instructions
|
||||
|
||||
\
|
||||
Orders summary
|
||||
================
|
||||
\
|
||||
Here is a summary of the orders for your products:
|
||||
= t :producer_mail_order_text
|
||||
\
|
||||
- @line_items.each_pair do |variant, line_item|
|
||||
#{variant.sku} - #{raw(variant.product.supplier.name)} - #{raw(variant.product_and_full_name)} (QTY: #{line_item.quantity}) @ #{line_item.single_money} = #{line_item.display_amount}
|
||||
- @grouped_line_items.each_pair do |product_and_full_name, line_items|
|
||||
#{line_items.first.variant.sku} - #{raw(line_items.first.product.supplier.name)} - #{raw(product_and_full_name)} (QTY: #{line_items.sum(&:quantity)}) @ #{line_items.first.single_money} = #{Spree::Money.new(line_items.sum(&:total), currency: line_items.first.currency)}
|
||||
\
|
||||
Thanks and best wishes,
|
||||
\
|
||||
Total: #{@total}
|
||||
\
|
||||
= t :producer_mail_text_after
|
||||
|
||||
#{t :producer_mail_signoff},
|
||||
#{@coordinator.name}
|
||||
#{@coordinator.address.address1}, #{@coordinator.address.city}, #{@coordinator.address.zipcode}
|
||||
#{@coordinator.phone}
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
%div.pad-top{bindonce: true}
|
||||
%product.animate-repeat{"ng-controller" => "ProductNodeCtrl",
|
||||
"ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons | properties: activeProperties) track by product.id ", "id" => "product-{{ product.id }}"}
|
||||
= render partial: "shop/products/summary"
|
||||
= render "shop/products/summary"
|
||||
%shop-variant{variant: 'product.master', "bo-if" => "!product.hasVariants", "id" => "variant-{{ product.master.id }}"}
|
||||
%shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id", "id" => "variant-{{ variant.id }}"}
|
||||
%shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id", "id" => "variant-{{ variant.id }}", "ng-class" => "{'out-of-stock': !variant.on_demand && variant.count_on_hand == 0}"}
|
||||
|
||||
%product{"ng-show" => "Products.loading"}
|
||||
.row.summary
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
%em
|
||||
= t :products_from
|
||||
%span
|
||||
%enterprise-modal
|
||||
%i.ofn-i_036-producers{"bo-text" => "enterprise.name"}
|
||||
%enterprise-modal
|
||||
%i.ofn-i_036-producers
|
||||
%span{"bo-bind" => "enterprise.name"}
|
||||
.small-2.medium-2.large-1.columns.text-center
|
||||
.taxon-flag
|
||||
%render-svg{path: "{{product.primary_taxon.icon}}"}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
= render :partial => 'spree/admin/shared/order_sub_menu'
|
||||
|
||||
%div{ ng: { controller: 'LineItemsCtrl' } }
|
||||
%save-bar{ save: "submit()", form: "bulk_order_form" }
|
||||
%save-bar{ form: "bulk_order_form", buttons: "[{ text: 'Save Changes', action: submit, class: 'red' }]" }
|
||||
.filters{ :class => "sixteen columns alpha" }
|
||||
.date_filter{ :class => "two columns alpha" }
|
||||
%label{ :for => 'start_date_filter' }
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
= render 'spree/shared/line_item_name', line_item: line_item
|
||||
|
||||
- if @order.insufficient_stock_lines.include? line_item
|
||||
- if @insufficient_stock_lines.include? line_item
|
||||
%span.out-of-stock
|
||||
= variant.in_stock? ? t(:insufficient_stock, :on_hand => variant.on_hand) : t(:out_of_stock)
|
||||
%br/
|
||||
@@ -30,7 +30,7 @@
|
||||
-# "price-breakdown-placement" => "left",
|
||||
-# "price-breakdown-animation" => true}
|
||||
%td.text-center.cart-item-quantity{"data-hook" => "cart_item_quantity"}
|
||||
= item_form.number_field :quantity, :min => 0, :class => "line_item_quantity", :size => 5
|
||||
= item_form.number_field :quantity, :min => 0, "ofn-on-hand" => variant.on_hand, "ng-model" => "line_item_#{line_item.id}", :class => "line_item_quantity", :size => 5
|
||||
%td.cart-item-total.text-right{"data-hook" => "cart_item_total"}
|
||||
= line_item.display_amount_with_adjustments.to_html unless line_item.quantity.nil?
|
||||
|
||||
|
||||
@@ -19,10 +19,12 @@
|
||||
%td.order5.text-right{"ng-class" => "{'credit' : order.total < 0, 'debit' : order.total > 0, 'paid' : order.total == 0}","bo-text" => "order.total | localizeCurrency"}
|
||||
%td.order6.text-right.show-for-large-up{"ng-class" => "{'credit' : order.outstanding_balance < 0, 'debit' : order.outstanding_balance > 0, 'paid' : order.outstanding_balance == 0}", "bo-text" => "order.outstanding_balance | localizeCurrency"}
|
||||
%td.order7.text-right{"ng-class" => "{'credit' : order.running_balance < 0, 'debit' : order.running_balance > 0, 'paid' : order.running_balance == 0}", "bo-text" => "order.running_balance | localizeCurrency"}
|
||||
%tr.payment-row{"ng-repeat" => "payment in order.payments"}
|
||||
%td.order1= t :payment
|
||||
%tr.payment-row{"ng-repeat" => "payment in order.payments", "ng-class" => "{'invalid': payment.state != 'completed'}"}
|
||||
%td.order1{"bo-text" => "payment.payment_method"}
|
||||
%td.order2{"bo-text" => "payment.updated_at"}
|
||||
%td.order3.show-for-large-up{"bo-text" => "payment.payment_method"}
|
||||
%td.order3.show-for-large-up
|
||||
%i{"ng-class" => "{'ofn-i_012-warning': payment.state == 'invalid' || payment.state == 'void' || payment.state == 'failed'}"}
|
||||
%span{"bo-text" => "'spree.payment_states.' + payment.state | t | capitalize"}
|
||||
%td.order4.show-for-large-up
|
||||
%td.order5.text-right{"ng-class" => "{'credit' : payment.amount > 0, 'debit' : payment.amount < 0, 'paid' : payment.amount == 0}","bo-text" => "payment.amount | localizeCurrency"}
|
||||
%td.order6.show-for-large-up
|
||||
|
||||
@@ -58,6 +58,13 @@ en-GB:
|
||||
|
||||
sort_order_cycles_on_shopfront_by: "Sort Order Cycles On Shopfront By"
|
||||
|
||||
# To customise text in emails.
|
||||
producer_mail_greeting: "Dear"
|
||||
producer_mail_text_before: "We now have all the consumer orders for the next food drop."
|
||||
producer_mail_order_text: "Here is a summary of the orders for your products:"
|
||||
producer_mail_delivery_instructions: "Stock pickup/delivery instructions:"
|
||||
producer_mail_text_after: "Please confirm that you have got this email. Please send me an invoice for this amount so that we can send you payment. If you need to phone me on the day, please use the number below."
|
||||
producer_mail_signoff: "Thanks and best wishes"
|
||||
|
||||
admin:
|
||||
# General form elements
|
||||
@@ -614,6 +621,10 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
products_distributor: Distributor
|
||||
products_distributor_info: When you select a distributor for your order, their address and pickup times will be displayed here.
|
||||
|
||||
shop_trial_length: "Shop Trial Length (Days)"
|
||||
shop_trial_expires_in: "Your shopfront trial expires in"
|
||||
shop_trial_expired_notice: "Open Food Network UK is currently free while we prepare for our soft launch in April, 2016."
|
||||
|
||||
# keys used in javascript
|
||||
password: Password
|
||||
remember_me: Remember Me
|
||||
@@ -1024,6 +1035,7 @@ Please follow the instructions there to make your enterprise visible on the Open
|
||||
pending: pending
|
||||
processing: processing
|
||||
void: void
|
||||
invalid: invalid
|
||||
order_state:
|
||||
address: address
|
||||
adjustments: adjustments
|
||||
@@ -1037,7 +1049,3 @@ Please follow the instructions there to make your enterprise visible on the Open
|
||||
resumed: resumed
|
||||
returned: returned
|
||||
skrill: skrill
|
||||
shop_trial_length: "Shop Trial Length (Days)"
|
||||
shop_trial_length: "Shop Trial Length (Days)"
|
||||
shop_trial_expires_in: "Your shopfront trial expires in"
|
||||
shop_trial_expired_notice: "Open Food Network UK is currently free while we prepare for our soft launch in April, 2016."
|
||||
|
||||
@@ -32,6 +32,11 @@ en:
|
||||
not_confirmed: Your email address could not be confirmed. Perhaps you have already completed this step?
|
||||
confirmation_sent: "Confirmation email sent!"
|
||||
confirmation_not_sent: "Could not send a confirmation email."
|
||||
enterprise_mailer:
|
||||
confirmation_instructions:
|
||||
subject: "Please confirm the email address for %{enterprise}"
|
||||
welcome:
|
||||
subject: "%{enterprise} is now on %{sitename}"
|
||||
home: "OFN"
|
||||
title: Open Food Network
|
||||
welcome_to: 'Welcome to '
|
||||
@@ -80,6 +85,10 @@ en:
|
||||
|
||||
whats_this: What's this?
|
||||
|
||||
tag_has_rules: "Existing rules for this tag: %{num}"
|
||||
has_one_rule: "has one rule"
|
||||
has_n_rules: "has %{num} rules"
|
||||
|
||||
customers:
|
||||
index:
|
||||
add_customer: "Add customer"
|
||||
@@ -401,6 +410,13 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
If you are a producer or food enterprise, we are excited to have you as a part of the network."
|
||||
email_signup_help_html: "We welcome all your questions and feedback; you can use the <em>Send Feedback</em> button on the site or email us at"
|
||||
|
||||
producer_mail_greeting: "Dear"
|
||||
producer_mail_text_before: "We now have all the consumer orders for the next food drop."
|
||||
producer_mail_order_text: "Here is a summary of the orders for your products:"
|
||||
producer_mail_delivery_instructions: "Stock pickup/delivery instructions:"
|
||||
producer_mail_text_after: ""
|
||||
producer_mail_signoff: "Thanks and best wishes"
|
||||
|
||||
shopping_oc_closed: Orders are closed
|
||||
shopping_oc_closed_description: "Please wait until the next cycle opens (or contact us directly to see if we can accept any late orders)"
|
||||
shopping_oc_last_closed: "The last cycle closed %{distance_of_time} ago"
|
||||
@@ -618,6 +634,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
products_distributor: Distributor
|
||||
products_distributor_info: When you select a distributor for your order, their address and pickup times will be displayed here.
|
||||
|
||||
shop_trial_length: "Shop Trial Length (Days)"
|
||||
shop_trial_expires_in: "Your shopfront trial expires in"
|
||||
shop_trial_expired_notice: "Good news! We have decided to extend shopfront trials until further notice."
|
||||
|
||||
|
||||
# keys used in javascript
|
||||
password: Password
|
||||
remember_me: Remember Me
|
||||
@@ -1028,6 +1049,7 @@ Please follow the instructions there to make your enterprise visible on the Open
|
||||
pending: pending
|
||||
processing: processing
|
||||
void: void
|
||||
invalid: invalid
|
||||
order_state:
|
||||
address: address
|
||||
adjustments: adjustments
|
||||
@@ -1041,6 +1063,3 @@ Please follow the instructions there to make your enterprise visible on the Open
|
||||
resumed: resumed
|
||||
returned: returned
|
||||
skrill: skrill
|
||||
shop_trial_length: "Shop Trial Length (Days)"
|
||||
shop_trial_expires_in: "Your shopfront trial expires in"
|
||||
shop_trial_expired_notice: "Good news! We have decided to extend shopfront trials until further notice (probably around March 2015)."
|
||||
|
||||
@@ -117,6 +117,8 @@ Openfoodnetwork::Application.routes.draw do
|
||||
|
||||
resources :customers, only: [:index, :create, :update, :destroy]
|
||||
|
||||
resources :tags, only: [:index], format: :json
|
||||
|
||||
resource :content
|
||||
|
||||
resource :accounts_and_billing_settings, only: [:edit, :update] do
|
||||
|
||||
@@ -34,13 +34,13 @@ describe CheckoutController do
|
||||
flash[:info].should == "The hub you have selected is temporarily closed for orders. Please try again later."
|
||||
end
|
||||
|
||||
it "redirects to the shop when no line items are present" do
|
||||
it "redirects to the cart when some items are out of stock" do
|
||||
controller.stub(:current_distributor).and_return(distributor)
|
||||
controller.stub(:current_order_cycle).and_return(order_cycle)
|
||||
controller.stub(:current_order).and_return(order)
|
||||
order.stub_chain(:insufficient_stock_lines, :present?).and_return true
|
||||
get :edit
|
||||
response.should redirect_to shop_path
|
||||
response.should redirect_to spree.cart_path
|
||||
end
|
||||
|
||||
it "renders when both distributor and order cycle is selected" do
|
||||
|
||||
@@ -2,13 +2,14 @@ require 'spec_helper'
|
||||
|
||||
describe EnterprisesController do
|
||||
describe "shopping for a distributor" do
|
||||
let(:order) { controller.current_order(true) }
|
||||
|
||||
before(:each) do
|
||||
@current_distributor = create(:distributor_enterprise, with_payment_and_shipping: true)
|
||||
@distributor = create(:distributor_enterprise, with_payment_and_shipping: true)
|
||||
@order_cycle1 = create(:simple_order_cycle, distributors: [@distributor], orders_open_at: 2.days.ago, orders_close_at: 3.days.from_now )
|
||||
@order_cycle2 = create(:simple_order_cycle, distributors: [@distributor], orders_open_at: 3.days.ago, orders_close_at: 4.days.from_now )
|
||||
controller.current_order(true).distributor = @current_distributor
|
||||
order.set_distributor! @current_distributor
|
||||
end
|
||||
|
||||
it "sets the shop as the distributor on the order when shopping for the distributor" do
|
||||
@@ -52,6 +53,27 @@ describe EnterprisesController do
|
||||
controller.current_order.line_items.size.should == 1
|
||||
end
|
||||
|
||||
describe "when an out of stock item is in the cart" do
|
||||
let(:variant) { create(:variant, on_demand: false, on_hand: 10) }
|
||||
let(:line_item) { create(:line_item, variant: variant) }
|
||||
let(:order_cycle) { create(:simple_order_cycle, distributors: [@distributor], variants: [variant]) }
|
||||
|
||||
before do
|
||||
order.set_distribution! @current_distributor, order_cycle
|
||||
order.line_items << line_item
|
||||
|
||||
Spree::Config.set allow_backorders: false
|
||||
variant.on_hand = 0
|
||||
variant.save!
|
||||
end
|
||||
|
||||
it "redirects to the cart" do
|
||||
spree_get :shop, {id: @current_distributor}
|
||||
|
||||
response.should redirect_to spree.cart_path
|
||||
end
|
||||
end
|
||||
|
||||
it "sets order cycle if only one is available at the chosen distributor" do
|
||||
@order_cycle2.destroy
|
||||
|
||||
|
||||
@@ -69,17 +69,6 @@ describe ShopController do
|
||||
end
|
||||
|
||||
|
||||
describe "producers/suppliers" do
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
let(:product) { create(:product, supplier: supplier) }
|
||||
let(:order_cycle) { create(:simple_order_cycle, distributors: [distributor]) }
|
||||
|
||||
before do
|
||||
exchange = order_cycle.exchanges.to_enterprises(distributor).outgoing.first
|
||||
exchange.variants << product.master
|
||||
end
|
||||
end
|
||||
|
||||
describe "returning products" do
|
||||
let(:order_cycle) { create(:simple_order_cycle, distributors: [distributor]) }
|
||||
let(:exchange) { order_cycle.exchanges.to_enterprises(distributor).outgoing.first }
|
||||
|
||||
@@ -15,6 +15,7 @@ describe Spree::OrdersController do
|
||||
controller.stub(:current_order_cycle).and_return(order_cycle)
|
||||
controller.stub(:current_order).and_return order
|
||||
order.stub_chain(:line_items, :empty?).and_return true
|
||||
order.stub(:insufficient_stock_lines).and_return []
|
||||
session[:access_token] = order.token
|
||||
spree_get :edit
|
||||
response.should redirect_to shop_path
|
||||
@@ -42,6 +43,88 @@ describe Spree::OrdersController do
|
||||
flash[:info].should == "The hub you have selected is temporarily closed for orders. Please try again later."
|
||||
end
|
||||
|
||||
describe "when an item has insufficient stock" do
|
||||
let(:order) { subject.current_order(true) }
|
||||
let(:oc) { create(:simple_order_cycle, distributors: [d], variants: [variant]) }
|
||||
let(:d) { create(:distributor_enterprise, shipping_methods: [create(:shipping_method)], payment_methods: [create(:payment_method)]) }
|
||||
let(:variant) { create(:variant, on_demand: false, on_hand: 5) }
|
||||
let(:line_item) { order.line_items.last }
|
||||
|
||||
before do
|
||||
Spree::Config.allow_backorders = false
|
||||
order.set_distribution! d, oc
|
||||
order.add_variant variant, 5
|
||||
variant.update_attributes! on_hand: 3
|
||||
end
|
||||
|
||||
it "displays a flash message when we view the cart" do
|
||||
spree_get :edit
|
||||
expect(response.status).to eq 200
|
||||
flash[:error].should == "An item in your cart has become unavailable."
|
||||
end
|
||||
end
|
||||
|
||||
describe "returning stock levels in JSON on success" do
|
||||
let(:product) { create(:simple_product) }
|
||||
|
||||
it "returns stock levels as JSON" do
|
||||
controller.stub(:variant_ids_in) { [123] }
|
||||
controller.stub(:stock_levels) { 'my_stock_levels' }
|
||||
Spree::OrderPopulator.stub(:new).and_return(populator = double())
|
||||
populator.stub(:populate) { true }
|
||||
populator.stub(:variants_h) { {} }
|
||||
|
||||
xhr :post, :populate, use_route: :spree, format: :json
|
||||
|
||||
data = JSON.parse(response.body)
|
||||
data['stock_levels'].should == 'my_stock_levels'
|
||||
end
|
||||
|
||||
describe "generating stock levels" do
|
||||
let!(:order) { create(:order) }
|
||||
let!(:li) { create(:line_item, order: order, variant: v, quantity: 2, max_quantity: 3) }
|
||||
let!(:v) { create(:variant, count_on_hand: 4) }
|
||||
let!(:v2) { create(:variant, count_on_hand: 2) }
|
||||
|
||||
before do
|
||||
order.reload
|
||||
controller.stub(:current_order) { order }
|
||||
end
|
||||
|
||||
it "returns a hash with variant id, quantity, max_quantity and stock on hand" do
|
||||
controller.stock_levels(order, [v.id]).should ==
|
||||
{v.id => {quantity: 2, max_quantity: 3, on_hand: 4}}
|
||||
end
|
||||
|
||||
it "includes all line items, even when the variant_id is not specified" do
|
||||
controller.stock_levels(order, []).should ==
|
||||
{v.id => {quantity: 2, max_quantity: 3, on_hand: 4}}
|
||||
end
|
||||
|
||||
it "includes an empty quantity entry for variants that aren't in the order" do
|
||||
controller.stock_levels(order, [v.id, v2.id]).should ==
|
||||
{v.id => {quantity: 2, max_quantity: 3, on_hand: 4},
|
||||
v2.id => {quantity: 0, max_quantity: 0, on_hand: 2}}
|
||||
end
|
||||
|
||||
describe "encoding Infinity" do
|
||||
let!(:v) { create(:variant, on_demand: true, count_on_hand: 0) }
|
||||
|
||||
it "encodes Infinity as a large, finite integer" do
|
||||
controller.stock_levels(order, [v.id]).should ==
|
||||
{v.id => {quantity: 2, max_quantity: 3, on_hand: 2147483647}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "extracts variant ids from the populator" do
|
||||
variants_h = [{:variant_id=>"900", :quantity=>2, :max_quantity=>nil},
|
||||
{:variant_id=>"940", :quantity=>3, :max_quantity=>3}]
|
||||
|
||||
controller.variant_ids_in(variants_h).should == [900, 940]
|
||||
end
|
||||
end
|
||||
|
||||
context "adding a group buy product to the cart" do
|
||||
it "sets a variant attribute for the max quantity" do
|
||||
distributor_product = create(:distributor_enterprise)
|
||||
@@ -59,7 +142,8 @@ describe Spree::OrdersController do
|
||||
|
||||
it "returns HTTP success when successful" do
|
||||
Spree::OrderPopulator.stub(:new).and_return(populator = double())
|
||||
populator.stub(:populate).and_return true
|
||||
populator.stub(:populate) { true }
|
||||
populator.stub(:variants_h) { {} }
|
||||
xhr :post, :populate, use_route: :spree, format: :json
|
||||
response.status.should == 200
|
||||
end
|
||||
@@ -68,7 +152,7 @@ describe Spree::OrdersController do
|
||||
Spree::OrderPopulator.stub(:new).and_return(populator = double())
|
||||
populator.stub(:populate).and_return false
|
||||
xhr :post, :populate, use_route: :spree, format: :json
|
||||
response.status.should == 402
|
||||
response.status.should == 412
|
||||
end
|
||||
|
||||
it "tells populator to overwrite" do
|
||||
@@ -78,11 +162,11 @@ describe Spree::OrdersController do
|
||||
end
|
||||
end
|
||||
|
||||
context "removing line items from cart" do
|
||||
describe "removing line items from cart" do
|
||||
describe "when I pass params that includes a line item no longer in our cart" do
|
||||
it "should silently ignore the missing line item" do
|
||||
order = subject.current_order(true)
|
||||
li = order.add_variant(create(:simple_product, on_hand: 110).master)
|
||||
li = order.add_variant(create(:simple_product, on_hand: 110).variants.first)
|
||||
spree_get :update, order: { line_items_attributes: {
|
||||
"0" => {quantity: "0", id: "9999"},
|
||||
"1" => {quantity: "99", id: li.id}
|
||||
|
||||
22
spec/features/admin/external_services_spec.rb
Normal file
22
spec/features/admin/external_services_spec.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
require 'spec_helper'
|
||||
|
||||
feature 'External services' do
|
||||
include AuthenticationWorkflow
|
||||
|
||||
describe "bugherd" do
|
||||
before do
|
||||
Spree::Config.bugherd_api_key = nil
|
||||
login_to_admin_section
|
||||
end
|
||||
|
||||
it "lets me set an API key" do
|
||||
visit spree.edit_admin_general_settings_path
|
||||
|
||||
fill_in 'bugherd_api_key', with: 'abc123'
|
||||
click_button 'Update'
|
||||
|
||||
page.should have_content 'General Settings has been successfully updated!'
|
||||
expect(Spree::Config.bugherd_api_key).to eq 'abc123'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -267,6 +267,9 @@ feature %q{
|
||||
|
||||
|
||||
scenario "updating an order cycle", js: true do
|
||||
# Make the page long enough to avoid the save bar overlaying the form
|
||||
page.driver.resize(1280, 3600)
|
||||
|
||||
# Given an order cycle with all the settings
|
||||
oc = create(:order_cycle)
|
||||
initial_variants = oc.variants.sort_by &:id
|
||||
@@ -359,6 +362,8 @@ feature %q{
|
||||
select 'Distributor fee 2', from: 'order_cycle_outgoing_exchange_2_enterprise_fees_0_enterprise_fee_id'
|
||||
|
||||
# And I click Update
|
||||
expect(page).to have_selector "#save-bar"
|
||||
save_screenshot('abc.png')
|
||||
click_button 'Update and Close'
|
||||
|
||||
# Then my order cycle should have been updated
|
||||
@@ -607,10 +612,6 @@ feature %q{
|
||||
page.all('tr.supplier').count.should == 3
|
||||
page.all('tr.distributor').count.should == 3
|
||||
|
||||
# When I save, then those exchanges should remain
|
||||
click_button 'Update'
|
||||
page.should have_content "Your order cycle has been updated."
|
||||
|
||||
oc.reload
|
||||
oc.suppliers.should match_array [supplier_managed, supplier_permitted, supplier_unmanaged]
|
||||
oc.coordinator.should == distributor_managed
|
||||
@@ -618,6 +619,9 @@ feature %q{
|
||||
end
|
||||
|
||||
scenario "editing an order cycle" do
|
||||
# Make the page long enough to avoid the save bar overlaying the form
|
||||
page.driver.resize(1280, 3600)
|
||||
|
||||
oc = create(:simple_order_cycle, { suppliers: [supplier_managed, supplier_permitted, supplier_unmanaged], coordinator: distributor_managed, distributors: [distributor_managed, distributor_permitted, distributor_unmanaged], name: 'Order Cycle 1' } )
|
||||
|
||||
visit edit_admin_order_cycle_path(oc)
|
||||
@@ -627,6 +631,7 @@ feature %q{
|
||||
page.find("tr.supplier-#{supplier_permitted.id} a.remove-exchange").click
|
||||
page.find("tr.distributor-#{distributor_managed.id} a.remove-exchange").click
|
||||
page.find("tr.distributor-#{distributor_permitted.id} a.remove-exchange").click
|
||||
|
||||
click_button 'Update'
|
||||
|
||||
# Then the exchanges should be removed
|
||||
@@ -698,10 +703,6 @@ feature %q{
|
||||
# I should be able to see but not toggle v2, because I don't have permission
|
||||
expect(page).to have_field "order_cycle_outgoing_exchange_0_variants_#{v2.id}", disabled: true
|
||||
|
||||
# When I save, any exchanges that I can't manage remain
|
||||
click_button 'Update'
|
||||
page.should have_content "Your order cycle has been updated."
|
||||
|
||||
oc.reload
|
||||
oc.suppliers.should match_array [supplier_managed, supplier_permitted, supplier_unmanaged]
|
||||
oc.coordinator.should == distributor_managed
|
||||
@@ -751,10 +752,6 @@ feature %q{
|
||||
# I should be able to see but not toggle v2, because I don't have permission
|
||||
expect(page).to have_field "order_cycle_incoming_exchange_0_variants_#{v2.id}", disabled: true
|
||||
|
||||
# When I save, any exchange that I can't manage remains
|
||||
click_button 'Update'
|
||||
page.should have_content "Your order cycle has been updated."
|
||||
|
||||
oc.reload
|
||||
oc.suppliers.should match_array [supplier_managed, supplier_permitted, supplier_unmanaged]
|
||||
oc.coordinator.should == distributor_managed
|
||||
@@ -868,6 +865,9 @@ feature %q{
|
||||
end
|
||||
|
||||
scenario "updating an order cycle" do
|
||||
# Make the page long enough to avoid the save bar overlaying the form
|
||||
page.driver.resize(1280, 3600)
|
||||
|
||||
# Given an order cycle with pickup time and instructions
|
||||
fee1 = create(:enterprise_fee, name: 'my fee', enterprise: enterprise)
|
||||
fee2 = create(:enterprise_fee, name: 'that fee', enterprise: enterprise)
|
||||
@@ -902,6 +902,7 @@ feature %q{
|
||||
# When I update, or update and close, both work
|
||||
click_button 'Update'
|
||||
page.should have_content 'Your order cycle has been updated.'
|
||||
|
||||
click_button 'Update and Close'
|
||||
|
||||
# Then my order cycle should have been updated
|
||||
|
||||
@@ -13,6 +13,8 @@ feature %q{
|
||||
let!(:distributor2) { create(:distributor_enterprise) }
|
||||
let!(:distributor_credit) { create(:distributor_enterprise) }
|
||||
let!(:distributor_without_orders) { create(:distributor_enterprise) }
|
||||
let!(:accounts_distributor) {create :distributor_enterprise}
|
||||
let!(:order_account_invoice) { create(:order, distributor: accounts_distributor, state: 'complete', user: user) }
|
||||
let!(:d1o1) { create(:completed_order_with_totals, distributor_id: distributor1.id, user_id: user.id, total: 10000)}
|
||||
let!(:d1o2) { create(:order_without_full_payment, distributor_id: distributor1.id, user_id: user.id, total: 5000)}
|
||||
let!(:d2o1) { create(:completed_order_with_totals, distributor_id: distributor2.id, user_id: user.id)}
|
||||
@@ -21,19 +23,24 @@ feature %q{
|
||||
|
||||
|
||||
before do
|
||||
Spree::Config.accounts_distributor_id = accounts_distributor.id
|
||||
credit_order.update!
|
||||
login_as user
|
||||
visit "/account"
|
||||
end
|
||||
|
||||
it "shows all hubs that have been ordered from with balance or credit" do
|
||||
# Single test to avoid re-rendering page
|
||||
expect(page).to have_content distributor1.name
|
||||
expect(page).to have_content distributor2.name
|
||||
expect(page).not_to have_content distributor_without_orders.name
|
||||
# Exclude the special Accounts & Billing distributor
|
||||
expect(page).not_to have_content accounts_distributor.name
|
||||
expect(page).to have_content distributor1.name + " " + "Balance due"
|
||||
expect(page).to have_content distributor_credit.name + " Credit"
|
||||
end
|
||||
|
||||
|
||||
it "reveals table of orders for distributors when clicked" do
|
||||
expand_active_table_node distributor1.name
|
||||
expect(page).to have_link "Order " + d1o1.number, href:"/orders/#{d1o1.number}"
|
||||
|
||||
57
spec/features/consumer/external_services_spec.rb
Normal file
57
spec/features/consumer/external_services_spec.rb
Normal file
@@ -0,0 +1,57 @@
|
||||
require 'spec_helper'
|
||||
|
||||
feature 'External services' do
|
||||
include AuthenticationWorkflow
|
||||
include WebHelper
|
||||
|
||||
describe "bugherd" do
|
||||
describe "limiting inclusion by environment" do
|
||||
before { Spree::Config.bugherd_api_key = 'abc123' }
|
||||
|
||||
it "is not included in test" do
|
||||
visit root_path
|
||||
expect(script_content(with: 'bugherd')).to be_nil
|
||||
end
|
||||
|
||||
it "is not included in dev" do
|
||||
Rails.env.stub(:development?) { true }
|
||||
visit root_path
|
||||
expect(script_content(with: 'bugherd')).to be_nil
|
||||
end
|
||||
|
||||
it "is included in staging" do
|
||||
Rails.env.stub(:staging?) { true }
|
||||
visit root_path
|
||||
expect(script_content(with: 'bugherd')).not_to be_nil
|
||||
end
|
||||
|
||||
it "is included in production" do
|
||||
Rails.env.stub(:production?) { true }
|
||||
visit root_path
|
||||
expect(script_content(with: 'bugherd')).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "in an environment where BugHerd is displayed" do
|
||||
before { Rails.env.stub(:staging?) { true } }
|
||||
|
||||
context "when there is no API key set" do
|
||||
before { Spree::Config.bugherd_api_key = nil }
|
||||
|
||||
it "does not include the BugHerd script" do
|
||||
visit root_path
|
||||
expect(script_content(with: 'bugherd')).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "when an API key is set" do
|
||||
before { Spree::Config.bugherd_api_key = 'abc123' }
|
||||
|
||||
it "includes the BugHerd script, with the correct API key" do
|
||||
visit root_path
|
||||
expect(script_content(with: 'bugherd')).to include 'abc123'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,25 +7,53 @@ feature "full-page cart", js: true do
|
||||
include UIComponentHelper
|
||||
|
||||
describe "viewing the cart" do
|
||||
let!(:zone) { create(:zone_with_member) }
|
||||
let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) }
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
let!(:order_cycle) { create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.variants.first]) }
|
||||
let(:enterprise_fee) { create(:enterprise_fee, amount: 11.00, tax_category: product.tax_category) }
|
||||
let(:product) { create(:taxed_product, supplier: supplier, zone: zone, price: 110.00, tax_rate_amount: 0.1) }
|
||||
let(:variant) { product.variants.first }
|
||||
let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) }
|
||||
|
||||
before do
|
||||
add_enterprise_fee enterprise_fee
|
||||
set_order order
|
||||
add_product_to_cart
|
||||
visit spree.cart_path
|
||||
end
|
||||
|
||||
describe "tax" do
|
||||
let!(:zone) { create(:zone_with_member) }
|
||||
let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) }
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
let!(:order_cycle) { create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.master]) }
|
||||
let(:enterprise_fee) { create(:enterprise_fee, amount: 11.00, tax_category: product.tax_category) }
|
||||
let(:product) { create(:taxed_product, supplier: supplier, zone: zone, price: 110.00, tax_rate_amount: 0.1) }
|
||||
let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) }
|
||||
|
||||
before do
|
||||
add_enterprise_fee enterprise_fee
|
||||
set_order order
|
||||
add_product_to_cart
|
||||
visit spree.cart_path
|
||||
end
|
||||
|
||||
it "shows the total tax for the order, including product tax and tax on fees" do
|
||||
page.should have_selector '.tax-total', text: '11.00' # 10 + 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "updating quantities with insufficient stock available" do
|
||||
let(:li) { order.line_items(true).last }
|
||||
|
||||
before do
|
||||
variant.update_attributes! on_hand: 2
|
||||
end
|
||||
|
||||
it "prevents me from entering an invalid value" do
|
||||
visit spree.cart_path
|
||||
|
||||
accept_alert 'Insufficient stock available, only 2 remaining' do
|
||||
fill_in "order_line_items_attributes_0_quantity", with: '4'
|
||||
end
|
||||
|
||||
page.should have_field "order_line_items_attributes_0_quantity", with: '2'
|
||||
end
|
||||
|
||||
it "shows the quantities saved, not those submitted" do
|
||||
fill_in "order_line_items_attributes_0_quantity", with: '4'
|
||||
|
||||
click_button 'Update'
|
||||
|
||||
page.should have_field "order[line_items_attributes][0][quantity]", with: '1'
|
||||
page.should have_content "Insufficient stock available, only 2 remaining"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,7 +9,7 @@ feature "As a consumer I want to check out my cart", js: true do
|
||||
|
||||
let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) }
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.master]) }
|
||||
let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.variants.first]) }
|
||||
let(:product) { create(:simple_product, supplier: supplier) }
|
||||
let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) }
|
||||
let(:address) { create(:address, firstname: "Foo", lastname: "Bar") }
|
||||
@@ -23,7 +23,7 @@ feature "As a consumer I want to check out my cart", js: true do
|
||||
|
||||
it "does not not render the login form when logged in" do
|
||||
quick_login_as user
|
||||
visit checkout_path
|
||||
visit checkout_path
|
||||
within "section[role='main']" do
|
||||
page.should_not have_content "Login"
|
||||
page.should have_checkout_details
|
||||
@@ -31,7 +31,7 @@ feature "As a consumer I want to check out my cart", js: true do
|
||||
end
|
||||
|
||||
it "renders the login buttons when logged out" do
|
||||
visit checkout_path
|
||||
visit checkout_path
|
||||
within "section[role='main']" do
|
||||
page.should have_content "Login"
|
||||
click_button "Login"
|
||||
@@ -53,9 +53,8 @@ feature "As a consumer I want to check out my cart", js: true do
|
||||
end
|
||||
|
||||
it "allows user to checkout as guest" do
|
||||
visit checkout_path
|
||||
visit checkout_path
|
||||
checkout_as_guest
|
||||
page.should have_checkout_details
|
||||
page.should have_checkout_details
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@ feature "As a consumer I want to check out my cart", js: true do
|
||||
let!(:zone) { create(:zone_with_member) }
|
||||
let(:distributor) { create(:distributor_enterprise, charges_sales_tax: true) }
|
||||
let(:supplier) { create(:supplier_enterprise) }
|
||||
let!(:order_cycle) { create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.master]) }
|
||||
let!(:order_cycle) { create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [variant]) }
|
||||
let(:enterprise_fee) { create(:enterprise_fee, amount: 1.23, tax_category: product.tax_category) }
|
||||
let(:product) { create(:taxed_product, supplier: supplier, price: 10, zone: zone, tax_rate_amount: 0.1) }
|
||||
let(:variant) { product.variants.first }
|
||||
let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) }
|
||||
|
||||
before do
|
||||
@@ -45,6 +46,22 @@ feature "As a consumer I want to check out my cart", js: true do
|
||||
distributor.shipping_methods << sm3
|
||||
end
|
||||
|
||||
describe "when I have an out of stock product in my cart" do
|
||||
before do
|
||||
Spree::Config.set allow_backorders: false
|
||||
variant.on_hand = 0
|
||||
variant.save!
|
||||
end
|
||||
|
||||
it "returns me to the cart with an error message" do
|
||||
visit checkout_path
|
||||
|
||||
page.should_not have_selector 'closing', text: "Checkout now"
|
||||
page.should have_selector 'closing', text: "Your shopping cart"
|
||||
page.should have_content "An item in your cart has become unavailable"
|
||||
end
|
||||
end
|
||||
|
||||
context "on the checkout page" do
|
||||
before do
|
||||
visit checkout_path
|
||||
@@ -213,6 +230,18 @@ feature "As a consumer I want to check out my cart", js: true do
|
||||
page.should have_content "Your order has been processed successfully"
|
||||
end
|
||||
|
||||
it "takes us to the cart page with an error when a product becomes out of stock just before we purchase", js: true do
|
||||
Spree::Config.set allow_backorders: false
|
||||
variant.on_hand = 0
|
||||
variant.save!
|
||||
|
||||
place_order
|
||||
|
||||
page.should_not have_content "Your order has been processed successfully"
|
||||
page.should have_selector 'closing', text: "Your shopping cart"
|
||||
page.should have_content "Out of Stock"
|
||||
end
|
||||
|
||||
context "when we are charged a shipping fee" do
|
||||
before { choose sm2.name }
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ feature "As a consumer I want to shop with a distributor", js: true do
|
||||
describe "with variants on the product" do
|
||||
let(:variant) { create(:variant, product: product, on_hand: 10 ) }
|
||||
before do
|
||||
add_product_and_variant_to_order_cycle(exchange, product, variant)
|
||||
add_variant_to_order_cycle(exchange, variant)
|
||||
set_order_cycle(order, oc1)
|
||||
visit shop_path
|
||||
end
|
||||
@@ -215,9 +215,11 @@ feature "As a consumer I want to shop with a distributor", js: true do
|
||||
let(:exchange) { Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) }
|
||||
let(:product) { create(:simple_product) }
|
||||
let(:variant) { create(:variant, product: product) }
|
||||
let(:variant2) { create(:variant, product: product) }
|
||||
|
||||
before do
|
||||
add_product_and_variant_to_order_cycle(exchange, product, variant)
|
||||
add_variant_to_order_cycle(exchange, variant)
|
||||
add_variant_to_order_cycle(exchange, variant2)
|
||||
set_order_cycle(order, oc1)
|
||||
visit shop_path
|
||||
end
|
||||
@@ -235,6 +237,111 @@ feature "As a consumer I want to shop with a distributor", js: true do
|
||||
|
||||
Spree::LineItem.where(id: li).should be_empty
|
||||
end
|
||||
|
||||
it "alerts us when we enter a quantity greater than the stock available" do
|
||||
variant.update_attributes on_hand: 5
|
||||
visit shop_path
|
||||
|
||||
accept_alert 'Insufficient stock available, only 5 remaining' do
|
||||
fill_in "variants[#{variant.id}]", with: '10'
|
||||
end
|
||||
|
||||
page.should have_field "variants[#{variant.id}]", with: '5'
|
||||
end
|
||||
|
||||
describe "when a product goes out of stock just before it's added to the cart" do
|
||||
it "stops the attempt, shows an error message and refreshes the products asynchronously" do
|
||||
variant.update_attributes! on_hand: 0
|
||||
|
||||
# -- Messaging
|
||||
fill_in "variants[#{variant.id}]", with: '1'
|
||||
wait_until { !cart_dirty }
|
||||
|
||||
within(".out-of-stock-modal") do
|
||||
page.should have_content "stock levels for one or more of the products in your cart have reduced"
|
||||
page.should have_content "#{product.name} - #{variant.unit_to_display} is now out of stock."
|
||||
end
|
||||
|
||||
# -- Page updates
|
||||
# Update amount in cart
|
||||
page.should have_field "variants[#{variant.id}]", with: '0', disabled: true
|
||||
page.should have_field "variants[#{variant2.id}]", with: ''
|
||||
|
||||
# Update amount available in product list
|
||||
# If amount falls to zero, variant should be greyed out and input disabled
|
||||
page.should have_selector "#variant-#{variant.id}.out-of-stock"
|
||||
page.should have_selector "#variants_#{variant.id}[ofn-on-hand='0']"
|
||||
page.should have_selector "#variants_#{variant.id}[disabled='disabled']"
|
||||
end
|
||||
|
||||
context "group buy products" do
|
||||
let(:product) { create(:simple_product, group_buy: true) }
|
||||
|
||||
it "does the same" do
|
||||
# -- Place in cart so we can set max_quantity, then make out of stock
|
||||
fill_in "variants[#{variant.id}]", with: '1'
|
||||
wait_until { !cart_dirty }
|
||||
variant.update_attributes! on_hand: 0
|
||||
|
||||
# -- Messaging
|
||||
fill_in "variant_attributes[#{variant.id}][max_quantity]", with: '1'
|
||||
wait_until { !cart_dirty }
|
||||
|
||||
within(".out-of-stock-modal") do
|
||||
page.should have_content "stock levels for one or more of the products in your cart have reduced"
|
||||
page.should have_content "#{product.name} - #{variant.unit_to_display} is now out of stock."
|
||||
end
|
||||
|
||||
# -- Page updates
|
||||
# Update amount in cart
|
||||
page.should have_field "variant_attributes[#{variant.id}][max_quantity]", with: '0', disabled: true
|
||||
|
||||
# Update amount available in product list
|
||||
# If amount falls to zero, variant should be greyed out and input disabled
|
||||
page.should have_selector "#variant-#{variant.id}.out-of-stock"
|
||||
page.should have_selector "#variants_#{variant.id}_max[disabled='disabled']"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the update is for another product" do
|
||||
it "updates quantity" do
|
||||
fill_in "variants[#{variant.id}]", with: '2'
|
||||
wait_until { !cart_dirty }
|
||||
|
||||
variant.update_attributes! on_hand: 1
|
||||
|
||||
fill_in "variants[#{variant2.id}]", with: '1'
|
||||
wait_until { !cart_dirty }
|
||||
|
||||
within(".out-of-stock-modal") do
|
||||
page.should have_content "stock levels for one or more of the products in your cart have reduced"
|
||||
page.should have_content "#{product.name} - #{variant.unit_to_display} now only has 1 remaining"
|
||||
end
|
||||
end
|
||||
|
||||
context "group buy products" do
|
||||
let(:product) { create(:simple_product, group_buy: true) }
|
||||
|
||||
it "does not update max_quantity" do
|
||||
fill_in "variants[#{variant.id}]", with: '2'
|
||||
fill_in "variant_attributes[#{variant.id}][max_quantity]", with: '3'
|
||||
wait_until { !cart_dirty }
|
||||
variant.update_attributes! on_hand: 1
|
||||
|
||||
fill_in "variants[#{variant2.id}]", with: '1'
|
||||
wait_until { !cart_dirty }
|
||||
|
||||
within(".out-of-stock-modal") do
|
||||
page.should have_content "stock levels for one or more of the products in your cart have reduced"
|
||||
page.should have_content "#{product.name} - #{variant.unit_to_display} now only has 1 remaining"
|
||||
end
|
||||
|
||||
page.should have_field "variants[#{variant.id}]", with: '1'
|
||||
page.should have_field "variant_attributes[#{variant.id}][max_quantity]", with: '3'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when no order cycles are available" do
|
||||
@@ -260,7 +367,7 @@ feature "As a consumer I want to shop with a distributor", js: true do
|
||||
let(:variant) { create(:variant, product: product) }
|
||||
|
||||
before do
|
||||
add_product_and_variant_to_order_cycle(exchange, product, variant)
|
||||
add_variant_to_order_cycle(exchange, variant)
|
||||
set_order_cycle(order, oc1)
|
||||
distributor.require_login = true
|
||||
distributor.save!
|
||||
|
||||
@@ -47,3 +47,32 @@ describe "CustomersCtrl", ->
|
||||
http.flush()
|
||||
expect(scope.customers.length).toBe 1
|
||||
expect(scope.customers[0]).not.toAngularEqual customer
|
||||
|
||||
describe "scope.findTags", ->
|
||||
tags = [
|
||||
{ text: 'one' }
|
||||
{ text: 'two' }
|
||||
{ text: 'three' }
|
||||
]
|
||||
beforeEach ->
|
||||
http.expectGET('/admin/tags.json?enterprise_id=1').respond 200, tags
|
||||
|
||||
it "retrieves the tag list", ->
|
||||
promise = scope.findTags('')
|
||||
result = null
|
||||
promise.then (data) ->
|
||||
result = data
|
||||
http.flush()
|
||||
expect(result).toAngularEqual tags
|
||||
|
||||
it "filters the tag list", ->
|
||||
filtered_tags = [
|
||||
{ text: 'two' }
|
||||
{ text: 'three' }
|
||||
]
|
||||
promise = scope.findTags('t')
|
||||
result = null
|
||||
promise.then (data) ->
|
||||
result = data
|
||||
http.flush()
|
||||
expect(result).toAngularEqual filtered_tags
|
||||
|
||||
@@ -10,7 +10,8 @@ describe "AdminSimpleEditOrderCycleCtrl", ->
|
||||
outgoing_exchange = {}
|
||||
|
||||
beforeEach ->
|
||||
scope = {}
|
||||
scope =
|
||||
$watch: jasmine.createSpy('$watch')
|
||||
location =
|
||||
absUrl: ->
|
||||
'example.com/admin/order_cycles/27/edit'
|
||||
|
||||
@@ -126,6 +126,85 @@ describe 'Cart service', ->
|
||||
$httpBackend.flush()
|
||||
expect(Cart.scheduleRetry).toHaveBeenCalled()
|
||||
|
||||
describe "verifying stock levels after update", ->
|
||||
describe "when an item is out of stock", ->
|
||||
it "reduces the quantity in the cart", ->
|
||||
li = {variant: {id: 1}, quantity: 5}
|
||||
stockLevels = {1: {quantity: 0, max_quantity: 0, on_hand: 0}}
|
||||
spyOn(Cart, 'line_items_present').andReturn [li]
|
||||
Cart.compareAndNotifyStockLevels stockLevels
|
||||
expect(li.quantity).toEqual 0
|
||||
expect(li.max_quantity).toBeUndefined()
|
||||
|
||||
it "reduces the max_quantity in the cart", ->
|
||||
li = {variant: {id: 1}, quantity: 5, max_quantity: 6}
|
||||
stockLevels = {1: {quantity: 0, max_quantity: 0, on_hand: 0}}
|
||||
spyOn(Cart, 'line_items_present').andReturn [li]
|
||||
Cart.compareAndNotifyStockLevels stockLevels
|
||||
expect(li.max_quantity).toEqual 0
|
||||
|
||||
it "resets the count on hand available", ->
|
||||
li = {variant: {id: 1, count_on_hand: 10}, quantity: 5}
|
||||
stockLevels = {1: {quantity: 0, max_quantity: 0, on_hand: 0}}
|
||||
spyOn(Cart, 'line_items_present').andReturn [li]
|
||||
Cart.compareAndNotifyStockLevels stockLevels
|
||||
expect(li.variant.count_on_hand).toEqual 0
|
||||
|
||||
describe "when the quantity available is less than that requested", ->
|
||||
it "reduces the quantity in the cart", ->
|
||||
li = {variant: {id: 1}, quantity: 6}
|
||||
stockLevels = {1: {quantity: 5, on_hand: 5}}
|
||||
spyOn(Cart, 'line_items_present').andReturn [li]
|
||||
Cart.compareAndNotifyStockLevels stockLevels
|
||||
expect(li.quantity).toEqual 5
|
||||
expect(li.max_quantity).toBeUndefined()
|
||||
|
||||
it "does not reduce the max_quantity in the cart", ->
|
||||
li = {variant: {id: 1}, quantity: 6, max_quantity: 7}
|
||||
stockLevels = {1: {quantity: 5, max_quantity: 5, on_hand: 5}}
|
||||
spyOn(Cart, 'line_items_present').andReturn [li]
|
||||
Cart.compareAndNotifyStockLevels stockLevels
|
||||
expect(li.max_quantity).toEqual 7
|
||||
|
||||
it "resets the count on hand available", ->
|
||||
li = {variant: {id: 1}, quantity: 6}
|
||||
stockLevels = {1: {quantity: 5, on_hand: 6}}
|
||||
spyOn(Cart, 'line_items_present').andReturn [li]
|
||||
Cart.compareAndNotifyStockLevels stockLevels
|
||||
expect(li.variant.count_on_hand).toEqual 6
|
||||
|
||||
describe "when the client-side quantity has been increased during the request", ->
|
||||
it "does not reset the quantity", ->
|
||||
li = {variant: {id: 1}, quantity: 6}
|
||||
stockLevels = {1: {quantity: 5, on_hand: 6}}
|
||||
spyOn(Cart, 'line_items_present').andReturn [li]
|
||||
Cart.compareAndNotifyStockLevels stockLevels
|
||||
expect(li.quantity).toEqual 6
|
||||
expect(li.max_quantity).toBeUndefined()
|
||||
|
||||
it "does not reset the max_quantity", ->
|
||||
li = {variant: {id: 1}, quantity: 5, max_quantity: 7}
|
||||
stockLevels = {1: {quantity: 5, max_quantity: 6, on_hand: 7}}
|
||||
spyOn(Cart, 'line_items_present').andReturn [li]
|
||||
Cart.compareAndNotifyStockLevels stockLevels
|
||||
expect(li.quantity).toEqual 5
|
||||
expect(li.max_quantity).toEqual 7
|
||||
|
||||
describe "when the client-side quantity has been changed from 0 to 1 during the request", ->
|
||||
it "does not reset the quantity", ->
|
||||
li = {variant: {id: 1}, quantity: 1}
|
||||
spyOn(Cart, 'line_items_present').andReturn [li]
|
||||
Cart.compareAndNotifyStockLevels {}
|
||||
expect(li.quantity).toEqual 1
|
||||
expect(li.max_quantity).toBeUndefined()
|
||||
|
||||
it "does not reset the max_quantity", ->
|
||||
li = {variant: {id: 1}, quantity: 1, max_quantity: 1}
|
||||
spyOn(Cart, 'line_items_present').andReturn [li]
|
||||
Cart.compareAndNotifyStockLevels {}
|
||||
expect(li.quantity).toEqual 1
|
||||
expect(li.max_quantity).toEqual 1
|
||||
|
||||
it "pops the queue", ->
|
||||
Cart.update_enqueued = true
|
||||
spyOn(Cart, 'scheduleUpdate')
|
||||
|
||||
@@ -5,7 +5,7 @@ describe 'Checkout service', ->
|
||||
Navigation = null
|
||||
flash = null
|
||||
scope = null
|
||||
FlashLoaderMock =
|
||||
FlashLoaderMock =
|
||||
loadFlash: (arg)->
|
||||
paymentMethods = [{
|
||||
id: 99
|
||||
@@ -41,10 +41,10 @@ describe 'Checkout service', ->
|
||||
|
||||
module 'Darkswarm'
|
||||
module ($provide)->
|
||||
$provide.value "RailsFlashLoader", FlashLoaderMock
|
||||
$provide.value "currentOrder", orderData
|
||||
$provide.value "shippingMethods", shippingMethods
|
||||
$provide.value "paymentMethods", paymentMethods
|
||||
$provide.value "RailsFlashLoader", FlashLoaderMock
|
||||
$provide.value "currentOrder", orderData
|
||||
$provide.value "shippingMethods", shippingMethods
|
||||
$provide.value "paymentMethods", paymentMethods
|
||||
null
|
||||
|
||||
inject ($injector, _$httpBackend_, $rootScope)->
|
||||
@@ -80,26 +80,33 @@ describe 'Checkout service', ->
|
||||
it 'Gets the current payment method', ->
|
||||
expect(Checkout.paymentMethod()).toEqual null
|
||||
Checkout.order.payment_method_id = 99
|
||||
expect(Checkout.paymentMethod()).toEqual paymentMethods[0]
|
||||
expect(Checkout.paymentMethod()).toEqual paymentMethods[0]
|
||||
|
||||
it "Posts the Checkout to the server", ->
|
||||
$httpBackend.expectPUT("/checkout", {order: Checkout.preprocess()}).respond 200, {path: "test"}
|
||||
Checkout.submit()
|
||||
$httpBackend.flush()
|
||||
|
||||
it "sends flash messages to the flash service", ->
|
||||
spyOn(FlashLoaderMock, "loadFlash") # Stubbing out writes to window.location
|
||||
$httpBackend.expectPUT("/checkout").respond 400, {flash: {error: "frogs"}}
|
||||
Checkout.submit()
|
||||
describe "when there is an error", ->
|
||||
it "redirects when a redirect is given", ->
|
||||
$httpBackend.expectPUT("/checkout").respond 400, {path: 'path'}
|
||||
Checkout.submit()
|
||||
$httpBackend.flush()
|
||||
expect(Navigation.go).toHaveBeenCalledWith 'path'
|
||||
|
||||
$httpBackend.flush()
|
||||
expect(FlashLoaderMock.loadFlash).toHaveBeenCalledWith {error: "frogs"}
|
||||
it "sends flash messages to the flash service", ->
|
||||
spyOn(FlashLoaderMock, "loadFlash") # Stubbing out writes to window.location
|
||||
$httpBackend.expectPUT("/checkout").respond 400, {flash: {error: "frogs"}}
|
||||
Checkout.submit()
|
||||
|
||||
it "puts errors into the scope", ->
|
||||
$httpBackend.expectPUT("/checkout").respond 400, {errors: {error: "frogs"}}
|
||||
Checkout.submit()
|
||||
$httpBackend.flush()
|
||||
expect(Checkout.errors).toEqual {error: "frogs"}
|
||||
$httpBackend.flush()
|
||||
expect(FlashLoaderMock.loadFlash).toHaveBeenCalledWith {error: "frogs"}
|
||||
|
||||
it "puts errors into the scope", ->
|
||||
$httpBackend.expectPUT("/checkout").respond 400, {errors: {error: "frogs"}}
|
||||
Checkout.submit()
|
||||
$httpBackend.flush()
|
||||
expect(Checkout.errors).toEqual {error: "frogs"}
|
||||
|
||||
describe "data preprocessing", ->
|
||||
beforeEach ->
|
||||
|
||||
@@ -169,7 +169,9 @@ describe 'OrderCycle controllers', ->
|
||||
EnterpriseFee = null
|
||||
|
||||
beforeEach ->
|
||||
scope = {}
|
||||
scope =
|
||||
order_cycle_form: jasmine.createSpyObj('order_cycle_form', ['$dirty', '$setPristine'])
|
||||
$watch: jasmine.createSpy('$watch')
|
||||
event =
|
||||
preventDefault: jasmine.createSpy('preventDefault')
|
||||
location =
|
||||
@@ -292,6 +294,7 @@ describe 'OrderCycle controllers', ->
|
||||
scope.removeExchange(event, 'exchange')
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
expect(OrderCycle.removeExchange).toHaveBeenCalledWith('exchange')
|
||||
expect(scope.order_cycle_form.$dirty).toEqual true
|
||||
|
||||
it 'Adds coordinator fees', ->
|
||||
scope.addCoordinatorFee(event)
|
||||
@@ -320,6 +323,7 @@ describe 'OrderCycle controllers', ->
|
||||
it 'Submits the order cycle via OrderCycle update', ->
|
||||
scope.submit('/admin/order_cycles')
|
||||
expect(OrderCycle.update).toHaveBeenCalledWith('/admin/order_cycles')
|
||||
expect(scope.order_cycle_form.$setPristine.calls.length).toEqual 1
|
||||
|
||||
|
||||
describe 'OrderCycle services', ->
|
||||
|
||||
@@ -12,7 +12,7 @@ describe EnterpriseMailer do
|
||||
EnterpriseMailer.confirmation_instructions(enterprise, 'token').deliver
|
||||
ActionMailer::Base.deliveries.count.should == 1
|
||||
mail = ActionMailer::Base.deliveries.first
|
||||
expect(mail.subject).to eq "Please confirm your email for #{enterprise.name}"
|
||||
expect(mail.subject).to eq "Please confirm the email address for #{enterprise.name}"
|
||||
expect(mail.to).to include enterprise.email
|
||||
expect(mail.reply_to).to be_nil
|
||||
end
|
||||
@@ -28,7 +28,7 @@ describe EnterpriseMailer do
|
||||
EnterpriseMailer.confirmation_instructions(enterprise, 'token').deliver
|
||||
ActionMailer::Base.deliveries.count.should == 1
|
||||
mail = ActionMailer::Base.deliveries.first
|
||||
expect(mail.subject).to eq "Please confirm your email for #{enterprise.name}"
|
||||
expect(mail.subject).to eq "Please confirm the email address for #{enterprise.name}"
|
||||
expect(mail.to).to include enterprise.unconfirmed_email
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,29 +2,34 @@ require 'spec_helper'
|
||||
require 'yaml'
|
||||
|
||||
describe ProducerMailer do
|
||||
let!(:zone) { create(:zone_with_member) }
|
||||
let!(:tax_rate) { create(:tax_rate, included_in_price: true, calculator: Spree::Calculator::DefaultTax.new, zone: zone, amount: 0.1) }
|
||||
let!(:tax_category) { create(:tax_category, tax_rates: [tax_rate]) }
|
||||
let(:s1) { create(:supplier_enterprise) }
|
||||
let(:s2) { create(:supplier_enterprise) }
|
||||
let(:s3) { create(:supplier_enterprise) }
|
||||
let(:d1) { create(:distributor_enterprise) }
|
||||
let(:d1) { create(:distributor_enterprise, charges_sales_tax: true) }
|
||||
let(:d2) { create(:distributor_enterprise) }
|
||||
let(:p1) { create(:product, price: 12.34, supplier: s1) }
|
||||
let(:p1) { create(:product, price: 12.34, supplier: s1, tax_category: tax_category) }
|
||||
let(:p2) { create(:product, price: 23.45, supplier: s2) }
|
||||
let(:p3) { create(:product, price: 34.56, supplier: s1) }
|
||||
let(:p4) { create(:product, price: 45.67, supplier: s1) }
|
||||
let(:order_cycle) { create(:simple_order_cycle) }
|
||||
let!(:incoming_exchange) { order_cycle.exchanges.create! sender: s1, receiver: d1, incoming: true, receival_instructions: 'Outside shed.' }
|
||||
|
||||
let!(:order) do
|
||||
order = create(:order, distributor: d1, order_cycle: order_cycle, state: 'complete')
|
||||
order.line_items << create(:line_item, variant: p1.master)
|
||||
order.line_items << create(:line_item, variant: p1.master)
|
||||
order.line_items << create(:line_item, variant: p2.master)
|
||||
order.line_items << create(:line_item, quantity: 1, variant: p1.variants.first)
|
||||
order.line_items << create(:line_item, quantity: 2, variant: p1.variants.first)
|
||||
order.line_items << create(:line_item, quantity: 3, variant: p2.variants.first)
|
||||
order.line_items << create(:line_item, quantity: 2, variant: p4.variants.first)
|
||||
order.finalize!
|
||||
order.save
|
||||
order
|
||||
end
|
||||
let!(:order_incomplete) do
|
||||
order = create(:order, distributor: d1, order_cycle: order_cycle, state: 'payment')
|
||||
order.line_items << create(:line_item, variant: p3.master)
|
||||
order.line_items << create(:line_item, variant: p3.variants.first)
|
||||
order.save
|
||||
order
|
||||
end
|
||||
@@ -44,7 +49,7 @@ describe ProducerMailer do
|
||||
end
|
||||
|
||||
it "includes receival instructions" do
|
||||
mail.body.should include 'Outside shed.'
|
||||
mail.body.encoded.should include 'Outside shed.'
|
||||
end
|
||||
|
||||
it "cc's the enterprise" do
|
||||
@@ -53,13 +58,28 @@ describe ProducerMailer do
|
||||
|
||||
it "contains an aggregated list of produce" do
|
||||
body_lines_including(mail, p1.name).each do |line|
|
||||
line.should include 'QTY: 2'
|
||||
line.should include '@ $10.00 = $20.00'
|
||||
line.should include 'QTY: 3'
|
||||
line.should include '@ $10.00 = $30.00'
|
||||
end
|
||||
body_as_html(mail).find("table.order-summary tr", text: p1.name)
|
||||
.should have_selector("td", text: "$30.00")
|
||||
end
|
||||
|
||||
it "displays tax totals for each product" do
|
||||
# Tax for p1 line items
|
||||
body_as_html(mail).find("table.order-summary tr", text: p1.name)
|
||||
.should have_selector("td", text: "$30.00")
|
||||
end
|
||||
|
||||
it "does not include incomplete orders" do
|
||||
mail.body.should_not include p3.name
|
||||
mail.body.encoded.should_not include p3.name
|
||||
end
|
||||
|
||||
it "includes the total" do
|
||||
# puts mail.text_part.body.encoded
|
||||
mail.body.encoded.should include 'Total: $50.00'
|
||||
body_as_html(mail).find("tr.total-row")
|
||||
.should have_selector("td", text: "$50.00")
|
||||
end
|
||||
|
||||
it "sends no mail when the producer has no orders" do
|
||||
@@ -74,4 +94,8 @@ describe ProducerMailer do
|
||||
def body_lines_including(mail, s)
|
||||
mail.body.to_s.lines.select { |line| line.include? s }
|
||||
end
|
||||
|
||||
def body_as_html(mail)
|
||||
Capybara.string(mail.html_part.body.encoded)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -42,6 +42,41 @@ module Spree
|
||||
end
|
||||
end
|
||||
|
||||
describe "capping quantity at stock level" do
|
||||
let!(:v) { create(:variant, on_demand: false, on_hand: 10) }
|
||||
let!(:li) { create(:line_item, variant: v, quantity: 10, max_quantity: 10) }
|
||||
|
||||
before do
|
||||
v.update_attributes! on_hand: 5
|
||||
end
|
||||
|
||||
it "caps quantity" do
|
||||
li.cap_quantity_at_stock!
|
||||
li.reload.quantity.should == 5
|
||||
end
|
||||
|
||||
it "does not cap max_quantity" do
|
||||
li.cap_quantity_at_stock!
|
||||
li.reload.max_quantity.should == 10
|
||||
end
|
||||
|
||||
it "works for products without max_quantity" do
|
||||
li.update_column :max_quantity, nil
|
||||
li.cap_quantity_at_stock!
|
||||
li.reload
|
||||
li.quantity.should == 5
|
||||
li.max_quantity.should be_nil
|
||||
end
|
||||
|
||||
it "does nothing for on_demand items" do
|
||||
v.update_attributes! on_demand: true
|
||||
li.cap_quantity_at_stock!
|
||||
li.reload
|
||||
li.quantity.should == 10
|
||||
li.max_quantity.should == 10
|
||||
end
|
||||
end
|
||||
|
||||
describe "calculating price with adjustments" do
|
||||
it "does not return fractional cents" do
|
||||
li = LineItem.new
|
||||
|
||||
@@ -149,13 +149,15 @@ module Spree
|
||||
end
|
||||
|
||||
describe "attempt_cart_add" do
|
||||
it "performs additional validations" do
|
||||
variant = double(:variant)
|
||||
quantity = 123
|
||||
let(:variant) { double(:variant, on_hand: 250) }
|
||||
let(:quantity) { 123 }
|
||||
|
||||
before do
|
||||
Spree::Variant.stub(:find).and_return(variant)
|
||||
VariantOverride.stub(:for).and_return(nil)
|
||||
end
|
||||
|
||||
op.should_receive(:check_stock_levels).with(variant, quantity).and_return(true)
|
||||
it "performs additional validations" do
|
||||
op.should_receive(:check_order_cycle_provided_for).with(variant).and_return(true)
|
||||
op.should_receive(:check_variant_available_under_distribution).with(variant).
|
||||
and_return(true)
|
||||
@@ -163,8 +165,76 @@ module Spree
|
||||
|
||||
op.attempt_cart_add(333, quantity.to_s)
|
||||
end
|
||||
|
||||
it "filters quantities through #quantities_to_add" do
|
||||
op.should_receive(:quantities_to_add).with(variant, 123, 123).
|
||||
and_return([5, 5])
|
||||
|
||||
op.stub(:check_order_cycle_provided_for) { true }
|
||||
op.stub(:check_variant_available_under_distribution) { true }
|
||||
|
||||
order.should_receive(:add_variant).with(variant, 5, 5, currency)
|
||||
|
||||
op.attempt_cart_add(333, quantity.to_s, quantity.to_s)
|
||||
end
|
||||
|
||||
it "removes variants which have become out of stock" do
|
||||
op.should_receive(:quantities_to_add).with(variant, 123, 123).
|
||||
and_return([0, 0])
|
||||
|
||||
op.stub(:check_order_cycle_provided_for) { true }
|
||||
op.stub(:check_variant_available_under_distribution) { true }
|
||||
|
||||
order.should_receive(:remove_variant).with(variant)
|
||||
order.should_receive(:add_variant).never
|
||||
|
||||
op.attempt_cart_add(333, quantity.to_s, quantity.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
describe "quantities_to_add" do
|
||||
let(:v) { double(:variant, on_hand: 10) }
|
||||
|
||||
context "when backorders are not allowed" do
|
||||
before { Spree::Config.allow_backorders = false }
|
||||
|
||||
context "when max_quantity is not provided" do
|
||||
it "returns full amount when available" do
|
||||
op.quantities_to_add(v, 5, nil).should == [5, nil]
|
||||
end
|
||||
|
||||
it "returns a limited amount when not entirely available" do
|
||||
op.quantities_to_add(v, 15, nil).should == [10, nil]
|
||||
end
|
||||
end
|
||||
|
||||
context "when max_quantity is provided" do
|
||||
it "returns full amount when available" do
|
||||
op.quantities_to_add(v, 5, 6).should == [5, 6]
|
||||
end
|
||||
|
||||
it "also returns the full amount when not entirely available" do
|
||||
op.quantities_to_add(v, 15, 16).should == [10, 16]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when backorders are allowed" do
|
||||
around do |example|
|
||||
Spree::Config.allow_backorders = true
|
||||
example.run
|
||||
Spree::Config.allow_backorders = false
|
||||
end
|
||||
|
||||
it "does not limit quantity" do
|
||||
op.quantities_to_add(v, 15, nil).should == [15, nil]
|
||||
end
|
||||
|
||||
it "does not limit max_quantity" do
|
||||
op.quantities_to_add(v, 15, 16).should == [15, 16]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
describe "determining if distributor can supply products in cart" do
|
||||
|
||||
@@ -366,6 +366,7 @@ describe Spree::Order do
|
||||
let(:order) { create(:order) }
|
||||
let(:v1) { create(:variant) }
|
||||
let(:v2) { create(:variant) }
|
||||
let(:v3) { create(:variant) }
|
||||
|
||||
before do
|
||||
order.add_variant v1
|
||||
@@ -376,6 +377,12 @@ describe Spree::Order do
|
||||
order.remove_variant v1
|
||||
order.line_items(:reload).map(&:variant).should == [v2]
|
||||
end
|
||||
|
||||
it "does nothing when there is no matching line item" do
|
||||
expect do
|
||||
order.remove_variant v3
|
||||
end.to change(order.line_items(:reload), :count).by(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe "emptying the order" do
|
||||
|
||||
@@ -53,23 +53,6 @@ describe Spree.user_class do
|
||||
create(:user)
|
||||
end.to enqueue_job ConfirmSignupJob
|
||||
end
|
||||
|
||||
it "should not create a customer" do
|
||||
expect do
|
||||
create(:user)
|
||||
end.to change(Customer, :count).by(0)
|
||||
end
|
||||
|
||||
describe "when a customer record exists" do
|
||||
let!(:customer) { create(:customer, user: nil) }
|
||||
|
||||
it "should not create a customer" do
|
||||
expect(customer.user).to be nil
|
||||
user = create(:user, email: customer.email)
|
||||
customer.reload
|
||||
expect(customer.user).to eq user
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "known_users" do
|
||||
@@ -107,11 +90,17 @@ describe Spree.user_class do
|
||||
let!(:d1_order_for_u2) { create(:completed_order_with_totals, distributor: distributor1, user_id: u2.id) }
|
||||
let!(:d1o3) { create(:order, state: 'cart', distributor: distributor1, user_id: u1.id) }
|
||||
let!(:d2o1) { create(:completed_order_with_totals, distributor: distributor2, user_id: u2.id) }
|
||||
let!(:accounts_distributor) {create :distributor_enterprise}
|
||||
let!(:order_account_invoice) { create(:order, distributor: accounts_distributor, state: 'complete', user: u1) }
|
||||
|
||||
let!(:completed_payment) { create(:payment, order: d1o1, state: 'completed') }
|
||||
let!(:payment) { create(:payment, order: d1o2, state: 'invalid') }
|
||||
let!(:payment) { create(:payment, order: d1o2, state: 'checkout') }
|
||||
|
||||
it "returns enterprises that the user has ordered from" do
|
||||
before do
|
||||
Spree::Config.accounts_distributor_id = accounts_distributor.id
|
||||
end
|
||||
|
||||
it "returns enterprises that the user has ordered from, excluding accounts distributor" do
|
||||
expect(u1.enterprises_ordered_from).to eq [distributor1.id]
|
||||
end
|
||||
|
||||
@@ -131,8 +120,8 @@ describe Spree.user_class do
|
||||
expect(u1.orders_by_distributor.first.distributed_orders).not_to include d1o3
|
||||
end
|
||||
|
||||
it "doesn't return uncompleted payments" do
|
||||
expect(u1.orders_by_distributor.first.distributed_orders.map(&:payments).flatten).not_to include payment
|
||||
it "doesn't return payments that are still at checkout stage" do
|
||||
expect(u1.orders_by_distributor.first.distributed_orders.map{|o| o.payments}.flatten).not_to include payment
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
14
spec/serializers/admin/customer_serializer_spec.rb
Normal file
14
spec/serializers/admin/customer_serializer_spec.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
describe Api::Admin::CustomerSerializer do
|
||||
let(:customer) { create(:customer, tag_list: "one, two, three") }
|
||||
let!(:tag_rule) { create(:tag_rule, enterprise: customer.enterprise, preferred_customer_tags: "two") }
|
||||
|
||||
it "serializes a customer" do
|
||||
serializer = Api::Admin::CustomerSerializer.new customer
|
||||
result = JSON.parse(serializer.to_json)
|
||||
expect(result['email']).to eq customer.email
|
||||
tags = result['tags']
|
||||
expect(tags.length).to eq 3
|
||||
expect(tags[0]).to eq({ "text" => 'one', "rules" => nil })
|
||||
expect(tags[1]).to eq({ "text" => 'two', "rules" => 1 })
|
||||
end
|
||||
end
|
||||
@@ -18,7 +18,7 @@ module ShopWorkflow
|
||||
|
||||
def add_product_to_cart
|
||||
populator = Spree::OrderPopulator.new(order, order.currency)
|
||||
populator.populate(variants: {product.master.id => 1})
|
||||
populator.populate(variants: {product.variants.first.id => 1})
|
||||
|
||||
# Recalculate fee totals
|
||||
order.update_distribution_charge!
|
||||
@@ -28,15 +28,10 @@ module ShopWorkflow
|
||||
find("dd a", text: name).trigger "click"
|
||||
end
|
||||
|
||||
def add_product_to_order_cycle(exchange, product)
|
||||
exchange.variants << product.master
|
||||
def add_variant_to_order_cycle(exchange, variant)
|
||||
exchange.variants << variant
|
||||
end
|
||||
|
||||
def add_product_and_variant_to_order_cycle(exchange, product, variant)
|
||||
exchange.variants << product.master
|
||||
exchange.variants << variant
|
||||
end
|
||||
|
||||
def set_order_cycle(order, order_cycle)
|
||||
order.update_attribute(:order_cycle, order_cycle)
|
||||
end
|
||||
|
||||
@@ -115,6 +115,24 @@ module WebHelper
|
||||
DirtyFormDialog.new(page)
|
||||
end
|
||||
|
||||
# Fetch the content of a script block
|
||||
# eg. script_content with: 'my-script.com'
|
||||
# Returns nil if not found
|
||||
# Raises an exception if multiple matching blocks are found
|
||||
def script_content(opts={})
|
||||
elems = page.all('script', visible: false)
|
||||
|
||||
elems = elems.to_a.select { |e| e.text(:all).include? opts[:with] } if opts[:with]
|
||||
|
||||
if elems.none?
|
||||
nil
|
||||
elsif elems.many?
|
||||
raise "Multiple results returned for script_content"
|
||||
else
|
||||
elems.first.text(:all)
|
||||
end
|
||||
end
|
||||
|
||||
# http://www.elabs.se/blog/53-why-wait_until-was-removed-from-capybara
|
||||
# Do not use this without good reason. Capybara's built-in waiting is very effective.
|
||||
def wait_until(secs=nil)
|
||||
|
||||
Reference in New Issue
Block a user