Auto-merged master into uk/order_cycle_report on deployment.

This commit is contained in:
Maikel
2016-05-11 08:20:05 +10:00
53 changed files with 523 additions and 101 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -54,7 +54,7 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, $modal, $roo
if li.quantity > li.variant.count_on_hand
li.quantity = li.variant.count_on_hand
scope.variants.push li.variant
if li.max_quantity > li.variant.count_on_hand
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

View File

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

View 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()"} }
&#10006;

View File

@@ -0,0 +1,11 @@
.autocomplete-template
%span.tag-with-rules{ ng: { if: "data.rules" } }
{{$getDisplayText()}}
%span.tag-with-rules{ ng: { if: "data.rules == 1" } }
&mdash;
= t 'admin.has_one_rule'
%span.tag-with-rules{ ng: { if: "data.rules > 1" } }
&mdash;
= t 'admin.has_n_rules', { num: '{{data.rules}}' }
%span{ ng: { if: "!data.rules" } }
{{$getDisplayText()}}

View File

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

View File

@@ -7,6 +7,6 @@
placeholder: "0",
"ofn-disable-scroll" => true,
"ng-model" => "variant.line_item.quantity",
max: "{{variant.on_demand && 9999 || variant.count_on_hand }}",
"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}}"}

View File

@@ -8,7 +8,7 @@
"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 }}",
"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,
@@ -18,6 +18,6 @@
"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 }}",
min: "{{variant.line_item.quantity}}",
name: "variant_attributes[{{variant.id}}][max_quantity]",
id: "variants_{{variant.id}}_max"}

View File

@@ -0,0 +1,3 @@
.tag-with-rules {
color: black;
}

View File

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

View File

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

View 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

View File

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

View File

@@ -16,6 +16,7 @@ Spree::OrdersController.class_eval do
# 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
@@ -28,6 +29,41 @@ Spree::OrdersController.class_eval do
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
@@ -55,6 +91,7 @@ Spree::OrdersController.class_eval do
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)

View File

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

View File

@@ -352,6 +352,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

View File

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

View File

@@ -23,4 +23,7 @@ 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

View File

@@ -44,12 +44,7 @@ Spree::LineItem.class_eval do
def cap_quantity_at_stock!
attrs = {}
attrs[:quantity] = variant.on_hand if quantity > variant.on_hand
attrs[:max_quantity] = variant.on_hand if (max_quantity || 0) > variant.on_hand
update_attributes!(attrs) if attrs.any?
update_attributes!(quantity: variant.on_hand) if quantity > variant.on_hand
end

View File

@@ -77,7 +77,7 @@ Spree::OrderPopulator.class_eval do
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, on_hand].min if max_quantity
max_quantity_to_add = max_quantity # max_quantity is not capped
[quantity_to_add, max_quantity_to_add]
end

View File

@@ -51,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 },
@@ -65,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
@@ -78,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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"} &copy; OpenStreetMap contributors

View File

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

View File

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

View File

@@ -1006,7 +1006,6 @@ Please follow the instructions there to make your enterprise visible on the Open
validation_msg_product_category_cant_be_blank: "^Product Category cant be blank"
validation_msg_tax_category_cant_be_blank: "^Tax Category can't be blank"
validation_msg_is_associated_with_an_exising_customer: "is associated with an existing customer"
spree:
shipment_states:
backorder: backorder
@@ -1024,6 +1023,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

View File

@@ -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"
@@ -1031,6 +1040,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

View File

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

View File

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

View 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

View File

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

View 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

View File

@@ -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.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(: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

View File

@@ -238,6 +238,17 @@ 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
@@ -259,7 +270,7 @@ feature "As a consumer I want to shop with a distributor", js: true do
# 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='0']"
page.should have_selector "#variants_#{variant.id}[ofn-on-hand='0']"
page.should have_selector "#variants_#{variant.id}[disabled='disabled']"
end
@@ -288,33 +299,32 @@ feature "As a consumer I want to shop with a distributor", js: true do
# 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[max='0']"
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: '1'
fill_in "variants[#{variant.id}]", with: '2'
wait_until { !cart_dirty }
variant.update_attributes! on_hand: 0
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} is now out of stock."
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 "updates max_quantity" do
fill_in "variants[#{variant.id}]", with: '1'
fill_in "variant_attributes[#{variant.id}][max_quantity]", with: '2'
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
@@ -325,6 +335,9 @@ feature "As a consumer I want to shop with a distributor", js: true 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

View File

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

View File

@@ -159,12 +159,12 @@ describe 'Cart service', ->
expect(li.quantity).toEqual 5
expect(li.max_quantity).toBeUndefined()
it "reduces the max_quantity in the cart", ->
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 5
expect(li.max_quantity).toEqual 7
it "resets the count on hand available", ->
li = {variant: {id: 1}, quantity: 6}

View File

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

View File

@@ -55,9 +55,9 @@ module Spree
li.reload.quantity.should == 5
end
it "caps max_quantity" do
it "does not cap max_quantity" do
li.cap_quantity_at_stock!
li.reload.max_quantity.should == 5
li.reload.max_quantity.should == 10
end
it "works for products without max_quantity" do

View File

@@ -213,8 +213,8 @@ module Spree
op.quantities_to_add(v, 5, 6).should == [5, 6]
end
it "returns a limited amount when not entirely available" do
op.quantities_to_add(v, 15, 16).should == [10, 10]
it "also returns the full amount when not entirely available" do
op.quantities_to_add(v, 15, 16).should == [10, 16]
end
end
end

View File

@@ -90,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
@@ -114,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

View 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

View File

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