Merge current master

This commit is contained in:
Steve Pettitt
2016-04-26 03:18:27 +01:00
33 changed files with 1101 additions and 188 deletions

View File

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

View File

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

View 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.

View File

@@ -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",
max: "{{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

@@ -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,
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]",
id: "variants_{{variant.id}}_max"}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,13 +12,19 @@ Spree::OrdersController.class_eval do
include OrderCyclesHelper
layout 'darkswarm'
# Patching to redirect to shop if order is empty
def edit
@order = current_order(true)
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
@@ -30,19 +36,53 @@ 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 +161,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

View File

@@ -43,6 +43,16 @@ Spree::LineItem.class_eval do
where('spree_adjustments.id IS NULL')
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?
end
def has_tax?
adjustments.included_tax.any?
end

View File

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

View File

@@ -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, on_hand].min if max_quantity
[quantity_to_add, max_quantity_to_add]
end
def cart_remove(variant_id)
variant = Spree::Variant.find(variant_id)
@order.remove_variant(variant)

View File

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

View File

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

View File

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

View File

@@ -21,18 +21,129 @@ en-GB:
invalid: |
Invalid email or password.
Were you a guest last time? Perhaps you need to create an account or reset your password.
enterprise_confirmations:
enterprise:
confirmed: Thankyou, your email address has been confirmed.
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."
home: "OFN"
title: Open Food Network
welcome_to: 'Welcome to '
site_meta_description: "We begin from the ground up. With farmers and growers ready to tell their stories proudly and truly. With distributors ready to connect people with products fairly and honestly. With buyers who believe that better weekly shopping decisions can…"
search_by_name: Search by name...
producers: UK Producers
producers_join: UK producers are now welcome to join Open Food Network UK.
charges_sales_tax: Charges sales tax?
print: "Print"
charges_sales_tax: Charges VAT?
print_invoice: "Print Invoice"
send_invoice: "Send Invoice"
resend_confirmation: "Resend Confirmation"
view_order: "View Order"
edit_order: "Edit Order"
ship_order: "Ship Order"
cancel_order: "Cancel Order"
confirm_send_invoice: "An invoice for this order will be sent to the customer. Are you sure you want to continue?"
confirm_resend_order_confirmation: "Are you sure you want to resend the order confirmation email?"
invoice: "Invoice"
percentage_of_sales: "%{percentage} of sales"
percentage_of_turnover: "Percentage of turnover"
monthly_cap_excl_tax: "monthly cap (excl. VAT)"
capped_at_cap: "capped at %{cap}"
per_month: "per month"
free: "free"
plus_tax: "plus GST"
total_monthly_bill_incl_tax: "Total Monthly Bill (Incl. Tax)"
say_no: "No"
say_yes: "Yes"
logo: "Logo (640x130)"
logo_mobile: "Mobile logo (75x26)"
logo_mobile_svg: "Mobile logo (SVG)"
sort_order_cycles_on_shopfront_by: "Sort Order Cycles On Shopfront By"
admin:
# General form elements
quick_search: Quick Search
clear_all: Clear All
producer: Producer
shop: Shop
product: Product
variant: Variant
columns: Columns
actions: Actions
viewing: "Viewing: %{current_view_name}"
whats_this: What's this?
customers:
index:
add_customer: "Add customer"
customer_placeholder: "customer@example.org"
inventory:
title: Inventory
description: Use this page to manage inventories for your enterprises. Any product details set here will override those set on the 'Products' page
sku: SKU
price: Price
on_hand: On Hand
on_demand: On Demand?
enable_reset: Enable Stock Level Reset?
inherit: Inherit?
add: Add
hide: Hide
select_a_shop: Select A Shop
review_now: Review Now
new_products_alert_message: There are %{new_product_count} new products available to add to your inventory.
currently_empty: Your inventory is currently empty
no_matching_products: No matching products found in your inventory
no_hidden_products: No products have been hidden from this inventory
no_matching_hidden_products: No hidden products match your search criteria
no_new_products: No new products are available to add to this inventory
no_matching_new_products: No new products match your search criteria
inventory_powertip: This is your inventory of products. To add products to your inventory, select 'New Products' from the Viewing dropdown.
hidden_powertip: These products have been hidden from your inventory and will not be available to add to your shop. You can click 'Add' to add a product to you inventory.
new_powertip: These products are available to be added to your inventory. Click 'Add' to add a product to your inventory, or 'Hide' to hide it from view. You can always change your mind later!
order_cycle:
choose_products_from: "Choose Products From:"
enterprise:
select_outgoing_oc_products_from: Select outgoing OC products from
enterprises:
form:
primary_details:
shopfront_requires_login: "Shopfront requires login?"
shopfront_requires_login_tip: "Choose whether customers must login to view the shopfront."
shopfront_requires_login_false: "Public"
shopfront_requires_login_true: "Require customers to login"
home:
hubs:
show_closed_shops: "Show closed shops"
hide_closed_shops: "Hide closed shops"
show_on_map: "Show all on the map"
shared:
register_call:
selling_on_ofn: "Interested in getting on the Open Food Network?"
register: "Register here"
shop:
messages:
login: "login"
register: "register"
contact: "contact"
require_customer_login: "This shop is for customers only."
require_login_html: "Please %{login} if you have an account already. Otherwise, %{register} to become a customer."
require_customer_html: "Please %{contact} %{enterprise} to become a customer."
# Printable Invoice Columns
invoice_column_item: "Item"
invoice_column_qty: "Qty"
invoice_column_tax: "VAT"
invoice_column_price: "Price"
logo: "Logo (640x130)" #FIXME
logo_mobile: "Mobile logo (75x26)" #FIXME
logo_mobile_svg: "Mobile logo (SVG)" #FIXME
home_hero: "Hero image"
home_show_stats: "Show statistics"
footer_logo: "Logo (220x76)"
@@ -46,11 +157,10 @@ en-GB:
footer_links_md: "Links"
footer_about_url: "About URL"
footer_tos_url: "Terms of Service URL"
invoice: "Invoice"
name: Name
first_name: First name
last_name: Last name
first_name: First Name
last_name: Last Name
email: Email
phone: Phone
next: Next
@@ -90,9 +200,9 @@ en-GB:
cart_empty: "Cart empty"
cart_edit: "Edit your cart"
card_number: Card number
card_securitycode: "Security code"
card_expiry_date: Expiry date
card_number: Card Number
card_securitycode: "Security Code"
card_expiry_date: Expiry Date
ofn_cart_headline: "Current cart for:"
ofn_cart_distributor: "Distributor:"
@@ -187,7 +297,7 @@ en-GB:
checkout_cart_total: Cart total
checkout_shipping_price: Shipping
checkout_total_price: Total
checkout_back_to_cart: "Back to cart"
checkout_back_to_cart: "Back to Cart"
order_paid: PAID
order_not_paid: NOT PAID
@@ -197,12 +307,32 @@ en-GB:
order_delivery_on: Delivery on
order_delivery_address: Delivery address
order_special_instructions: "Your notes:"
order_pickup_instructions: Collection instructions
order_pickup_time: Ready for collection
order_pickup_instructions: Collection Instructions
order_produce: Produce
order_total_price: Total
order_includes_tax: (includes tax)
order_payment_paypal_successful: Your payment via PayPal has been processed successfully.
order_hub_info: Hub info
order_hub_info: Hub Info
bom_tip: "Use this page to alter product quantities across multiple orders. Products may also be removed from orders entirely, if required."
bom_shared: "Shared Resource?"
bom_page_title: "Bulk Order Management"
bom_no: "Order no."
bom_date: "Order date"
bom_cycle: "Order cycle"
bom_max: "Max"
bom_hub: "Hub"
bom_variant: "Product: Unit"
bom_final_weigth_volume: "Weight/Volume"
bom_quantity: "Quantity"
bom_actions_delete: "Delete Selected"
bom_loading: "Loading orders"
bom_no_results: "No orders found."
bom_order_error: "Some errors must be resolved before you can update orders.\nAny fields with red borders contain errors."
unsaved_changes_warning: "Unsaved changes exist and will be lost if you continue."
unsaved_changes_error: "Fields with red borders contain errors."
products: "Products"
products_in: "in %{oc}"
@@ -307,6 +437,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using
products_cart_empty: "Cart empty"
products_edit_cart: "Edit your cart"
products_from: from
products_change: "No changes to save."
products_update_error: "Saving failed with the following error(s):"
products_update_error_msg: "Saving failed."
products_update_error_data: "Save failed due to invalid data:"
products_changes_saved: "Changes saved."
search_no_results_html: "Sorry, no results found for %{query}. Try another search?"
@@ -317,9 +452,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using
groups_title: Groups
groups_headline: Groups / regions
groups_text: "Every producer is unique. Every business has something different to offer. Our groups are collectives of producers, hubs and distributors who share something in common like location, farmers market or philosophy. This makes your shopping experience easier. So explore our groups and have the curating done for you."
groups_search: "Search name or keyword"
groups_no_groups: "No groups found"
groups_about: "About Us"
groups_producers: "Our producers"
groups_hubs: "Our hubs"
groups_contact_web: Contact
@@ -384,9 +521,9 @@ See the %{link} to find out more about %{sitename}'s features and to start using
ocs_close_time: "ORDERS CLOSE"
ocs_when_headline: When do you want your order?
ocs_when_text: No products are displayed until you select a date.
ocs_when_closing: "Closing on"
ocs_when_closing: "Closing On"
ocs_when_choose: "Choose Order Cycle"
ocs_list: "List view"
ocs_list: "List View"
producers_about: About us
producers_buy: Shop for
@@ -407,13 +544,26 @@ See the %{link} to find out more about %{sitename}'s features and to start using
producers_signup_cta_headline: Join now!
producers_signup_cta_action: Join now
producers_signup_detail: Here's the detail.
producer: Producer
products_item: Item
products_description: Description
products_variant: Variant
products_quantity: Quantity
products_availabel: Available?
products_price: Price
products_producer: "Producer"
products_price: "Price"
products_sku: "SKU"
products_name: "name"
products_unit: "unit"
products_on_hand: "on hand"
products_on_demand: "On demand?"
products_category: "Category"
products_tax_category: "tax category"
products_available_on: "Available On"
products_inherit: "Inherit?"
products_inherits_properties: "Inherits Properties?"
products_stock_level_reset: "Enable Stock Level Reset?"
register_title: Register
@@ -431,7 +581,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using
shops_signup_detail: Here's the detail.
orders_fees: Fees...
orders_edit_title: Shopping cart
orders_edit_title: Shopping Cart
orders_edit_headline: Your shopping cart
orders_edit_time: Order ready for
orders_edit_continue: Continue shopping
@@ -509,18 +659,17 @@ See the %{link} to find out more about %{sitename}'s features and to start using
confirm_password: "Confirm password"
action_signup: "Sign up now"
welcome_to_ofn: "Welcome to the Open Food Network!"
signup_or_login: "Start By signing up (or logging in)"
signup_or_login: "Start By Signing Up (or logging in)"
have_an_account: "Already have an account?"
action_login: "Log in now."
forgot_password: "Forgot password?"
forgot_password: "Forgot Password?"
password_reset_sent: "An email with instructions on resetting your password has been sent!"
reset_password: "Reset password"
registration_greeting: "Greetings!"
who_is_managing_enterprise: "Who is responsible for managing %{enterprise}?"
enterprise_contact: "Primary contact"
enterprise_contact: "Primary Contact"
enterprise_contact_required: "You need to enter a primary contact."
enterprise_email: "Email address"
enterprise_email_required: "You need to enter valid email address."
enterprise_email_address: "Email address"
enterprise_phone: "Phone number"
back: "Back"
continue: "Continue"
@@ -528,20 +677,20 @@ See the %{link} to find out more about %{sitename}'s features and to start using
limit_reached_message: "You have reached the limit!"
limit_reached_text: "You have reached the limit for the number of enterprises you are allowed to own on the"
limit_reached_action: "Return to the homepage"
select_promo_image: "Step 3. Select promo image"
select_promo_image: "Step 3. Select Promo Image"
promo_image_tip: "Tip: Shown as a banner, preferred size is 1200×260px"
promo_image_label: "Choose a promo image"
action_or: "OR"
promo_image_drag: "Drag and drop your promo here"
review_promo_image: "Step 4. Review your promo banner"
review_promo_image: "Step 4. Review Your Promo Banner"
review_promo_image_tip: "Tip: for best results, your promo image should fill the available space"
promo_image_placeholder: "Your logo will appear here for review once uploaded"
uploading: "Uploading..."
select_logo: "Step 1. Select logo image"
select_logo: "Step 1. Select Logo Image"
logo_tip: "Tip: Square images will work best, preferably at least 300×300px"
logo_label: "Choose a logo image"
logo_drag: "Drag and drop your logo here"
review_logo: "Step 2. Review your logo"
review_logo: "Step 2. Review Your Logo"
review_logo_tip: "Tip: for best results, your logo should fill the available space"
logo_placeholder: "Your logo will appear here for review once uploaded"
enterprise_about_headline: "Nice one!"
@@ -598,14 +747,14 @@ Please follow the instructions there to make your enterprise visible on the Open
registration_type_error: "Please choose one. Are you are producer?"
registration_type_producer_help: "Producers make yummy things to eat and/or drink. You're a producer if you grow it, raise it, brew it, bake it, ferment it, milk it or mould it."
registration_type_no_producer_help: "If youre not a producer, youre probably someone who sells and distributes food. You might be a hub, coop, buying group, retailer, wholesaler or other."
create_profile: "Create profile"
create_profile: "Create Profile"
registration_images_headline: "Thanks!"
registration_images_description: "Let's upload some pretty pictures so your profile looks great! :)"
registration_detail_headline: "Let's get started"
registration_detail_headline: "Let's Get Started"
registration_detail_enterprise: "Woot! First we need to know a little bit about your enterprise:"
registration_detail_producer: "Woot! First we need to know a little bit about your farm:"
registration_detail_name_enterprise: "Enterprise name:"
registration_detail_name_producer: "Farm name:"
registration_detail_name_enterprise: "Enterprise Name:"
registration_detail_name_producer: "Farm Name:"
registration_detail_name_placeholder: "e.g. Charlie's Awesome Farm"
registration_detail_name_error: "Please choose a unique name for your enterprise"
registration_detail_address1: "Address line 1:"
@@ -641,6 +790,222 @@ Please follow the instructions there to make your enterprise visible on the Open
price_graph: "Price graph"
included_tax: "Included tax"
remove_tax: "Remove tax"
balance: "Balance"
transaction: "Transaction"
transaction_date: "Date" #Transaction is only in key to avoid conflict with :date
payment_state: "Payment status"
shipping_state: "Shipping status"
value: "Value"
balance_due: "Balance due"
credit: "Credit"
Paid: "Paid"
Ready: "Ready"
you_have_no_orders_yet: "You have no orders yet"
running_balance: "Running balance"
outstanding_balance: "Outstanding balance"
admin_entreprise_relationships: "Enterprise Relationships"
admin_entreprise_relationships_everything: "Everything"
admin_entreprise_relationships_permits: "permits"
admin_entreprise_relationships_seach_placeholder: "Search"
admin_entreprise_relationships_button_create: "Create"
admin_entreprise_groups: "Enterprise Groups"
admin_entreprise_groups_name: "Name"
admin_entreprise_groups_owner: "Owner"
admin_entreprise_groups_on_front_page: "On front page ?"
admin_entreprise_groups_entreprise: "Enterprises"
admin_entreprise_groups_primary_details: "Primary Details"
admin_entreprise_groups_data_powertip: "The primary user responsible for this group."
admin_entreprise_groups_data_powertip_logo: "This is the logo for the group"
admin_entreprise_groups_data_powertip_promo_image: "This image is displayed at the top of the Group profile"
admin_entreprise_groups_about: "About"
admin_entreprise_groups_images: "Images"
admin_entreprise_groups_contact: "Contact"
admin_entreprise_groups_contact_phone_placeholder: "eg. 98 7654 3210"
admin_entreprise_groups_contact_address1_placeholder: "eg. 123 High Street"
admin_entreprise_groups_contact_city: "Suburb"
admin_entreprise_groups_contact_city_placeholder: "eg. Northcote"
admin_entreprise_groups_contact_zipcode: "Postcode"
admin_entreprise_groups_contact_zipcode_placeholder: "eg. 3070"
admin_entreprise_groups_contact_state_id: "State"
admin_entreprise_groups_contact_country_id: "Country"
admin_entreprise_groups_web: "Web Resources"
admin_entreprise_groups_web_twitter: "eg. @the_prof"
admin_entreprise_groups_web_website_placeholder: "eg. www.truffles.com"
admin_order_cycles: "Admin Order Cycles"
open: "Open"
close: "Close"
supplier: "Supplier"
coordinator: "Coordinator"
distributor: "Distributor"
product: "Products"
enterprise_fees: "Enterprise Fees"
fee_type: "Fee Type"
tax_category: "Tax Category"
calculator: "Calculator"
calculator_values: "Calculator values"
new_order_cycles: "New Order Cycles"
select_a_coordinator_for_your_order_cycle: "select a coordinator for your order cycle"
edit_order_cycle: "Edit Order Cycle"
roles: "Roles"
update: "Update"
add_producer_property: "Add producer property"
admin_settings: "Settings"
update_invoice: "Update Invoices"
finalise_invoice: "Finalise Invoices"
finalise_user_invoices: "Finalise User Invoices"
finalise_user_invoice_explained: "Use this button to finalize all invoices in the system for the previous calendar month. This task can be set up to run automatically once a month."
manually_run_task: "Manually Run Task "
update_user_invoices: "Update User Invoices"
update_user_invoice_explained: "Use this button to immediately update invoices for the month to date for each enterprise user in the system. This task can be set up to run automatically every night."
auto_finalise_invoices: "Auto-finalise invoices monthly on the 2nd at 1:30am"
auto_update_invoices: "Auto-update invoices nightly at 1:00am"
in_progress: "In Progress"
started_at: "Started at"
queued: "Queued"
scheduled_for: "Scheduled for"
customers: "Customers"
please_select_hub: "Please select a Hub"
loading_customers: "Loading Customers"
no_customers_found: "No customers found"
go: "Go"
hub: "Hub"
accounts_administration_distributor: "accounts administration distributor"
accounts_and_billing: "Accounts & Billing"
producer: "Producer"
product: "Product"
price: "Price"
on_hand: "On hand"
save_changes: "Save Changes"
spree_admin_overview_enterprises_header: "My Enterprises"
spree_admin_overview_enterprises_footer: "MANAGE MY ENTERPRISES"
spree_admin_enterprises_hubs_name: "Name"
spree_admin_enterprises_create_new: "CREATE NEW"
spree_admin_enterprises_shipping_methods: "Shipping Methods"
spree_admin_enterprises_fees: "Enterprise Fees"
spree_admin_enterprises_none_create_a_new_enterprise: "CREATE A NEW ENTERPRISE"
spree_admin_enterprises_none_text: "You don't have any enterprises yet"
spree_admin_enterprises_producers_name: "Name"
spree_admin_enterprises_producers_total_products: "Total Products"
spree_admin_enterprises_producers_active_products: "Active Products"
spree_admin_enterprises_producers_order_cycles: "Products in OCs"
spree_admin_enterprises_producers_order_cycles_title: ""
spree_admin_enterprises_tabs_hubs: "HUBS"
spree_admin_enterprises_tabs_producers: "PRODUCERS"
spree_admin_enterprises_producers_manage_order_cycles: "MANAGE ORDER CYCLES"
spree_admin_enterprises_producers_manage_products: "MANAGE PRODUCTS"
spree_admin_enterprises_producers_orders_cycle_text: "You don't have any active order cycles."
spree_admin_enterprises_any_active_products_text: "You don't have any active products."
spree_admin_enterprises_create_new_product: "CREATE A NEW PRODUCT"
spree_admin_order_cycles: "Order Cycles"
spree_admin_order_cycles_tip: "Order cycles determine when and where your products are available to customers."
dashbord: "Dashboard"
spree_admin_single_enterprise_alert_mail_confirmation: "Please confirm the email address for"
spree_admin_single_enterprise_alert_mail_sent: "We've sent an email to"
spree_admin_overview_action_required: "Action Required"
spree_admin_overview_check_your_inbox: "Please check you inbox for furher instructions. Thanks!"
change_package: "Change Package"
spree_admin_single_enterprise_hint: "Hint: To allow people to find you, turn on your visibility under"
your_profil_live: "Your profile live"
on_ofn_map: "on the Open Food Network map"
see: "See"
live: "live"
manage: "Manage"
resend: "Resend"
add_and_manage_products: "Add & manage products"
add_and_manage_order_cycles: "Add & manage order cycles"
manage_order_cycles: "Manage order cycles"
manage_products: "Manage products"
edit_profile_details: "Edit profile details"
edit_profile_details_etc: "Change your profile description, images, etc."
start_date: "Start Date"
end_date: "End Date"
order_cycle: "Order Cycle"
group_buy_unit_size: "Group Buy Unit Size"
total_qtt_ordered: "Total Quantity Ordered"
max_qtt_ordered: "Max Quantity Ordered"
current_fulfilled_units: "Current Fulfilled Units"
max_fulfilled_units: "Max Fulfilled Units"
bulk_management_warning: "WARNING: Some variants do not have a unit value"
ask: "Ask?"
no_orders_found: "No orders found."
order_no: "Order No."
weight_volume: "Weight/Volume"
remove_tax: "Remove tax"
tax_settings: "Tax Settings"
products_require_tax_category: "products require tax category"
admin_shared_address_1: "Address"
admin_shared_address_2: "Address (cont.)"
admin_share_city: "City"
admin_share_zipcode: "Postcode"
admin_share_country: "Country"
admin_share_state: "State"
hub_sidebar_hubs: "Hubs"
hub_sidebar_none_available: "None Available"
hub_sidebar_manage: "Manage"
hub_sidebar_at_least: "At least one hub must be selected"
hub_sidebar_blue: "blue"
hub_sidebar_red: "red"
shop_trial_in_progress: "Your shopfront trial expires in %{days}."
shop_trial_expired: "Good news! We have decided to extend shopfront trials until further notice (probably around March 2015)." #FIXME
report_customers_distributor: "Distributor"
report_customers_supplier: "Supplier"
report_customers_cycle: "Order Cycle"
report_customers_type: "Report Type"
report_customers_csv: "Download as csv"
report_producers: "Producers: "
report_type: "Report Type: "
report_hubs: "Hubs: "
report_payment: "Payment Methods: "
report_distributor: "Distributor: "
report_payment_by: 'Payments By Type'
report_itemised_payment: 'Itemised Payment Totals'
report_payment_totals: 'Payment Totals'
report_all: 'all'
report_order_cycle: "Order Cycle: "
report_entreprises: "Enterprises: "
report_users: "Users: "
initial_invoice_number: "Initial invoice number:"
invoice_date: "Invoice date:"
due_date: "Due date:"
account_code: "Account code:"
equals: "Equals"
contains: "contains"
discount: "Discount"
filter_products: "Filter Products"
delete_product_variant: "The last variant cannot be deleted!"
progress: "progress"
saving: "Saving.."
success: "success"
failure: "failure"
unsaved_changes_confirmation: "Unsaved changes will be lost. Continue anyway?"
one_product_unsaved: "Changes to one product remain unsaved."
products_unsaved: "Changes to %{n} products remain unsaved."
add_manager: "Add a manager"
is_already_manager: "is already a manager!"
no_change_to_save: " No change to save"
add_manager: "Add a manager"
users: "Users"
about: "About"
images: "Images"
contact: "Contact"
web: "Web"
primary_details: "Primary Details"
adrdress: "Address"
contact: "Contact"
social: "Social"
business_details: "Business Details"
properties: "Properties"
shipping_methods: "Shipping Methods"
payment_methods: "Payment Methods"
enterprise_fees: "Enterprise Fees"
inventory_settings: "Inventory Settings"
tag_rules: "Tag Rules"
shop_preferences: "Shop Preferences"
validation_msg_relationship_already_established: "^That relationship is already established."
validation_msg_at_least_one_hub: "^At least one hub must be selected"
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

View File

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

View File

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

View File

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

View File

@@ -42,6 +42,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 +141,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 +151,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 +161,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}

View File

@@ -11,7 +11,7 @@ feature "full-page cart", js: true 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!(: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) }

View File

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

View File

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

View File

@@ -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,98 @@ feature "As a consumer I want to shop with a distributor", js: true do
Spree::LineItem.where(id: li).should be_empty
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}[max='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[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'
wait_until { !cart_dirty }
variant.update_attributes! on_hand: 0
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."
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'
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
end
end
end
end
context "when no order cycles are available" do
@@ -260,7 +354,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!

View File

@@ -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 "reduces 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
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')

View File

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

View File

@@ -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 "caps max_quantity" do
li.cap_quantity_at_stock!
li.reload.max_quantity.should == 5
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

View File

@@ -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 "returns a limited amount when not entirely available" do
op.quantities_to_add(v, 15, 16).should == [10, 10]
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

View File

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

View File

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

View File

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