spree_core 0.70.RC1 → 0.70.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. data/README.md +10 -4
  2. data/app/assets/javascripts/admin/admin.js.erb +1 -1
  3. data/app/assets/javascripts/admin/orders/edit_form.js +3 -1
  4. data/app/assets/stylesheets/admin/admin-form.css.erb +1 -3
  5. data/app/assets/stylesheets/admin/admin-tables.css.erb +1 -1
  6. data/app/assets/stylesheets/admin/admin.css.erb +2 -2
  7. data/app/assets/stylesheets/admin/spree_core.css +0 -1
  8. data/app/controllers/admin/line_items_controller.rb +3 -1
  9. data/app/controllers/checkout_controller.rb +6 -0
  10. data/app/helpers/spree/base_helper.rb +21 -10
  11. data/app/models/creditcard.rb +15 -1
  12. data/app/models/inventory_unit.rb +6 -10
  13. data/app/models/line_item.rb +17 -2
  14. data/app/models/order.rb +5 -3
  15. data/app/models/product.rb +33 -21
  16. data/app/models/taxonomy.rb +1 -1
  17. data/app/views/admin/mail_methods/_form.html.erb +3 -3
  18. data/app/views/admin/orders/_line_item.html.erb +3 -1
  19. data/app/views/admin/orders/index.html.erb +3 -3
  20. data/app/views/admin/payments/source_views/_gateway.html.erb +1 -1
  21. data/app/views/admin/products/_form.html.erb +3 -3
  22. data/app/views/admin/products/new.html.erb +3 -3
  23. data/app/views/admin/shared/_report_criteria.html.erb +3 -3
  24. data/app/views/admin/variants/_form.html.erb +3 -3
  25. data/app/views/admin/variants/index.html.erb +1 -1
  26. data/app/views/layouts/admin.html.erb +1 -1
  27. data/app/views/order_mailer/confirm_email.text.erb +1 -1
  28. data/app/views/orders/_line_item.html.erb +7 -2
  29. data/config/initializers/rails_3_1.rb +12 -0
  30. data/config/locales/en.yml +2 -0
  31. data/lib/generators/spree/dummy/dummy_generator.rb +1 -0
  32. data/lib/generators/spree/site/site_generator.rb +7 -7
  33. data/lib/scopes/product.rb +6 -6
  34. data/lib/spree_base.rb +5 -6
  35. data/lib/spree_core/railtie.rb +1 -1
  36. data/lib/spree_core/version.rb +1 -1
  37. metadata +70 -68
  38. data/app/assets/stylesheets/admin/grids.css +0 -314
  39. data/config/initializers/disable_paperclip_log.rb +0 -2
data/README.md CHANGED
@@ -1,15 +1,21 @@
1
+ Core
2
+ ====
3
+
4
+ Core e-commerce functionality for the Spree project
5
+
6
+
1
7
  Testing
2
- =======
8
+ -------
3
9
 
4
10
  Create the test site
5
11
 
6
- rake test_app
12
+ bundle exec rake test_app
7
13
 
8
14
  Run the tests
9
15
 
10
- rake spec
16
+ bundle exec rake spec
11
17
 
12
18
  Run the coverage. After the rake task open coverage/index.html
13
19
 
14
- rake rcov
20
+ bundle exec rake rcov
15
21
 
@@ -106,7 +106,7 @@ prep_product_autocomplete_data = function(data){
106
106
  }
107
107
 
108
108
  $.fn.product_autocomplete = function(){
109
- $(this).autocomplete("/admin/products.json?authenticity_token=" + escape($('meta[name=csrf-token]').attr("content")), {
109
+ $(this).autocomplete("/admin/products.json?authenticity_token=" + encodeURIComponent($('meta[name=csrf-token]').attr("content")), {
110
110
  parse: prep_product_autocomplete_data,
111
111
  formatItem: function(item) {
112
112
  return format_product_autocomplete(item);
@@ -9,7 +9,9 @@ $.each($('td.qty input'), function(i, inpt){
9
9
  type: "POST",
10
10
  url: "/admin/orders/" + $('input#order_number').val() + "/line_items/" + $(id).val(),
11
11
  data: ({_method: "put", "line_item[quantity]": $(this).val()}),
12
- success: function(html){ $('#order-form-wrapper').html(html)}
12
+ complete: function(resp){
13
+ $('#order-form-wrapper').html(resp.responseText);
14
+ }
13
15
  });
14
16
 
15
17
  }, 0,5);
@@ -53,9 +53,7 @@ fieldset#preferences textarea { height: 100px; }
53
53
 
54
54
 
55
55
  .date-range-filter {width:220px;}
56
- .date-range-filter input {width:75px;}
57
- .date-range-filter .yui-u { width:50% }
58
-
56
+ .date-range-filter input {width:70px;}
59
57
 
60
58
  /* Multi-column form layout
61
59
  -------------------------------------------------------------- */
@@ -24,7 +24,7 @@ table.index th {
24
24
  table.index tr.alt td {
25
25
  background-color: #efefef; }
26
26
  table.index.green th {
27
- background: #cfefa7 url(<%= asset_path 'admin/grid_header_back_green.png' %>) top left repeat-x;
27
+ background: #cfefa7 url(<%= asset_path 'admin/bg/grid_header_back_green.png' %>) top left repeat-x;
28
28
  border-left: 1px solid #E4FDB4;
29
29
  border-top: 1px solid #E4FDB4;
30
30
  border-right: 1px solid #B7CB90;
@@ -331,7 +331,7 @@ body {
331
331
  .errorExplanation h2 {
332
332
  font-size: 1.75em;
333
333
  padding-left: 40px;
334
- background: url(<%= asset_path 'admin/icons/orb/32x32/3.png' %>) center left no-repeat; }
334
+ background: url(<%= asset_path 'admin/icons/32x32/3.png' %>) center left no-repeat; }
335
335
  ul.checkbox-list {
336
336
  list-style: none; }
337
337
  ul.checkbox-list li {
@@ -406,7 +406,7 @@ body {
406
406
  ul.taxonomy-actions li.disabled {
407
407
  opacity: .5; }
408
408
  h3.warning {
409
- background-image: url(<%= asset_path 'admin/icons/fugue/exclamation.png' %>);
409
+ background-image: url(<%= asset_path 'admin/icons/exclamation.png' %>);
410
410
  background-position: 10px 15px;
411
411
  background-repeat: no-repeat;
412
412
  padding-left: 35px; }
@@ -10,7 +10,6 @@
10
10
  *= require admin/admin-typography.css
11
11
  *= require admin/admin
12
12
  *= require admin/edit_checkouts.css
13
- *= require admin/grids.css
14
13
  *= require admin/token-input.css
15
14
  *= require jquery-ui.datepicker
16
15
  */
@@ -14,7 +14,9 @@ class Admin::LineItemsController < Admin::BaseController
14
14
  format.html { render :partial => "admin/orders/form", :locals => {:order => @order.reload}, :layout => false }
15
15
  end
16
16
  else
17
- #TODO Handle failure gracefully, patches welcome.
17
+ respond_with(@line_item) do |format|
18
+ format.js { render :action => 'create', :locals => {:order => @order.reload}, :layout => false }
19
+ end
18
20
  end
19
21
  end
20
22
 
@@ -61,11 +61,17 @@ class CheckoutController < Spree::BaseController
61
61
  def load_order
62
62
  @order = current_order
63
63
  redirect_to cart_path and return unless @order and @order.checkout_allowed?
64
+ raise_insufficient_quantity and return if @order.insufficient_stock_lines.present?
64
65
  redirect_to cart_path and return if @order.completed?
65
66
  @order.state = params[:state] if params[:state]
66
67
  state_callback(:before)
67
68
  end
68
69
 
70
+ def raise_insufficient_quantity
71
+ flash[:error] = t('spree_inventory_error_flash_for_insufficient_quantity')
72
+ redirect_to cart_path
73
+ end
74
+
69
75
  def state_callback(before_or_after = :before)
70
76
  method_name = :"#{before_or_after}_#{@order.state}"
71
77
  send(method_name) if respond_to?(method_name, true)
@@ -35,8 +35,19 @@ module Spree::BaseHelper
35
35
 
36
36
  # human readable list of variant options
37
37
  def variant_options(v, allow_back_orders = Spree::Config[:allow_backorders], include_style = true)
38
+ ActiveSupport::Deprecation.warn("variant_options method is deprecated, and will be removed in 0.80.0", caller)
38
39
  list = v.options_text
39
- list = include_style ? content_tag(:span, "(#{t(:out_of_stock)}) #{list}", :class => "out-of-stock") : "#{t(:out_of_stock)} #{list}" unless (allow_back_orders || v.in_stock?)
40
+
41
+ # We shouldn't show out of stock if the product is infact in stock
42
+ # or when we're not allowing backorders.
43
+ unless (allow_back_orders || v.in_stock?)
44
+ list = if include_style
45
+ content_tag(:span, "(#{t(:out_of_stock)}) #{list}", :class => "out-of-stock")
46
+ else
47
+ "#{t(:out_of_stock)} #{list}"
48
+ end
49
+ end
50
+
40
51
  list
41
52
  end
42
53
 
@@ -56,12 +67,12 @@ module Spree::BaseHelper
56
67
  def meta_data_tags
57
68
  object = instance_variable_get('@'+controller_name.singularize)
58
69
  meta = { :keywords => Spree::Config[:default_meta_keywords], :description => Spree::Config[:default_meta_description] }
59
-
70
+
60
71
  if object.kind_of?(ActiveRecord::Base)
61
72
  meta[:keywords] = object.meta_keywords if object[:meta_keywords].present?
62
73
  meta[:description] = object.meta_description if object[:meta_description].present?
63
74
  end
64
-
75
+
65
76
  meta.map do |name, content|
66
77
  tag('meta', :name => name, :content => content)
67
78
  end.join("\n")
@@ -88,7 +99,7 @@ module Spree::BaseHelper
88
99
  def logo(image_path=Spree::Config[:logo])
89
100
  link_to image_tag(image_path), root_path
90
101
  end
91
-
102
+
92
103
  def flash_messages
93
104
  [:notice, :error].map do |msg_type|
94
105
  if flash[msg_type]
@@ -98,7 +109,7 @@ module Spree::BaseHelper
98
109
  end
99
110
  end.join("\n").html_safe
100
111
  end
101
-
112
+
102
113
  def breadcrumbs(taxon, separator="&nbsp;&raquo;&nbsp;")
103
114
  return "" if current_page?("/") || taxon.nil?
104
115
  separator = raw(separator)
@@ -120,8 +131,8 @@ module Spree::BaseHelper
120
131
  root_taxon.children.map do |taxon|
121
132
  css_class = (current_taxon && current_taxon.self_and_ancestors.include?(taxon)) ? 'current' : nil
122
133
  content_tag :li, :class => css_class do
123
- link_to(taxon.name, seo_url(taxon)) +
124
- taxons_tree(taxon, current_taxon, max_level - 1)
134
+ link_to(taxon.name, seo_url(taxon)) +
135
+ taxons_tree(taxon, current_taxon, max_level - 1)
125
136
  end
126
137
  end.join("\n").html_safe
127
138
  end
@@ -131,7 +142,7 @@ module Spree::BaseHelper
131
142
  return Country.all unless zone = Zone.find_by_name(Spree::Config[:checkout_zone])
132
143
  zone.country_list
133
144
  end
134
-
145
+
135
146
  def format_price(price, options={})
136
147
  options.assert_valid_keys(:show_vat_text)
137
148
  options.reverse_merge! :show_vat_text => Spree::Config[:show_price_inc_vat]
@@ -142,7 +153,7 @@ module Spree::BaseHelper
142
153
  formatted_price
143
154
  end
144
155
  end
145
-
156
+
146
157
  # generates nested url to product based on supplied taxon
147
158
  def seo_url(taxon, product = nil)
148
159
  return '/t/' + taxon.permalink if product.nil?
@@ -150,7 +161,7 @@ module Spree::BaseHelper
150
161
  "not used anymore. Use product_url instead. (called from #{caller[0]})"
151
162
  return product_url(product)
152
163
  end
153
-
164
+
154
165
  def current_orders_product_count
155
166
  if current_order.blank? || current_order.item_count < 1
156
167
  return 0
@@ -2,6 +2,7 @@ class Creditcard < ActiveRecord::Base
2
2
  has_many :payments, :as => :source
3
3
 
4
4
  before_save :set_last_digits
5
+ after_validation :set_card_type
5
6
 
6
7
  attr_accessor :number, :verification_value
7
8
 
@@ -23,6 +24,19 @@ class Creditcard < ActiveRecord::Base
23
24
  self.last_digits ||= number.to_s.length <= 4 ? number : number.to_s.slice(-4..-1)
24
25
  end
25
26
 
27
+ # cheap hack to get to the type? method from deep within ActiveMerchant without stomping on
28
+ # potentially existing methods in CreditCard
29
+ class CardDetector
30
+ class << self
31
+ include ActiveMerchant::Billing::CreditCardMethods::ClassMethods
32
+ end
33
+ end
34
+
35
+ # sets self.cc_type while we still have the card number
36
+ def set_card_type
37
+ self.cc_type ||= CardDetector.type?(self.number)
38
+ end
39
+
26
40
  def name?
27
41
  first_name? && last_name?
28
42
  end
@@ -243,7 +257,7 @@ class Creditcard < ActiveRecord::Base
243
257
 
244
258
  def spree_cc_type
245
259
  return "visa" if ENV['RAILS_ENV'] == "development"
246
- self.class.type?(number)
260
+ self.cc_type
247
261
  end
248
262
 
249
263
  # Saftey check to make sure we're not accidentally performing operations on a live gateway.
@@ -22,12 +22,6 @@ class InventoryUnit < ActiveRecord::Base
22
22
  after_transition :to => 'returned', :do => :restock_variant
23
23
  end
24
24
 
25
- # method deprecated in favour of adjust_units (which creates & destroys units as needed).
26
- def self.sell_units(order)
27
- warn "[DEPRECATION] `InventoryUnits#sell_units` is deprecated. Please use `InventoryUnits#assign_opening_inventory` instead. (called from #{caller[0]})"
28
- self.adjust_units(order)
29
- end
30
-
31
25
  # Assigns inventory to a newly completed order.
32
26
  # Should only be called once during the life-cycle of an order, on transition to completed.
33
27
  #
@@ -88,7 +82,10 @@ class InventoryUnit < ActiveRecord::Base
88
82
  end
89
83
 
90
84
  def self.destroy_units(order, variant, quantity)
91
- variant_units = order.inventory_units.group_by(&:variant_id)[variant.id].sort_by(&:state)
85
+ variant_units = order.inventory_units.group_by(&:variant_id)
86
+ return unless variant_units.include? variant.id
87
+
88
+ variant_units = variant_units[variant.id].sort_by(&:state)
92
89
 
93
90
  quantity.times do
94
91
  inventory_unit = variant_units.shift
@@ -97,9 +94,8 @@ class InventoryUnit < ActiveRecord::Base
97
94
  end
98
95
 
99
96
  def self.create_units(order, variant, sold, back_order)
100
- if back_order > 0 && !Spree::Config[:allow_backorders]
101
- raise "Cannot request back orders when backordering is disabled"
102
- end
97
+ return if back_order > 0 && !Spree::Config[:allow_backorders]
98
+
103
99
 
104
100
  shipment = order.shipments.detect {|shipment| !shipment.shipped? }
105
101
 
@@ -10,6 +10,8 @@ class LineItem < ActiveRecord::Base
10
10
  validates :variant, :presence => true
11
11
  validates :quantity, :numericality => { :only_integer => true, :message => I18n.t("validation.must_be_int") }
12
12
  validates :price, :numericality => true
13
+ validate :stock_availability
14
+
13
15
  # validate :meta_validation_of_quantities
14
16
 
15
17
  attr_accessible :quantity
@@ -58,6 +60,14 @@ class LineItem < ActiveRecord::Base
58
60
  self.quantity = 0 if self.quantity.nil? || self.quantity < 0
59
61
  end
60
62
 
63
+ def sufficient_stock?
64
+ Spree::Config[:allow_backorders] ? true : (self.variant.on_hand >= self.quantity)
65
+ end
66
+
67
+ def insufficient_stock?
68
+ !sufficient_stock?
69
+ end
70
+
61
71
  private
62
72
  def update_inventory
63
73
  return true unless self.order.completed?
@@ -89,7 +99,12 @@ class LineItem < ActiveRecord::Base
89
99
  if order.try(:inventory_units).to_a.any?{|unit| unit.variant_id == variant_id && unit.shipped?}
90
100
  errors.add :base, I18n.t("cannot_destory_line_item_as_inventory_units_have_shipped")
91
101
  return false
92
- end
93
- end
102
+ end
103
+ end
104
+
105
+ def stock_availability
106
+ return if sufficient_stock?
107
+ errors.add(:quantiy, "can't be greater than avaiable stock.")
108
+ end
94
109
  end
95
110
 
data/app/models/order.rb CHANGED
@@ -45,8 +45,6 @@ class Order < ActiveRecord::Base
45
45
 
46
46
  make_permalink :field => :number
47
47
 
48
- attr_accessor :out_of_stock_items
49
-
50
48
  class_attribute :update_hooks
51
49
  self.update_hooks = Set.new
52
50
 
@@ -318,7 +316,7 @@ class Order < ActiveRecord::Base
318
316
  # Called after transition to complete state when payments will have been processed
319
317
  def finalize!
320
318
  update_attribute(:completed_at, Time.now)
321
- self.out_of_stock_items = InventoryUnit.assign_opening_inventory(self)
319
+ InventoryUnit.assign_opening_inventory(self)
322
320
  # lock any optional adjustments (coupon promotions, etc.)
323
321
  adjustments.optional.each { |adjustment| adjustment.update_attribute("locked", true) }
324
322
  OrderMailer.confirm_email(self).deliver
@@ -378,6 +376,10 @@ class Order < ActiveRecord::Base
378
376
  line_items.map{|li| li.variant.product}
379
377
  end
380
378
 
379
+ def insufficient_stock_lines
380
+ line_items.select &:insufficient_stock?
381
+ end
382
+
381
383
  private
382
384
  def create_user
383
385
  self.email = user.email if self.user and not user.anonymous?
@@ -44,18 +44,17 @@ class Product < ActiveRecord::Base
44
44
  after_save :save_master
45
45
 
46
46
  has_many :variants,
47
- :conditions => ["variants.is_master = ? AND variants.deleted_at IS NULL", false],
48
- :order => 'variants.position ASC'
49
-
47
+ :conditions => ["#{Variant.table_name}.is_master = ? AND #{Variant.table_name}.deleted_at IS NULL", false],
48
+ :order => "#{Variant.table_name}.position ASC"
50
49
 
51
50
  has_many :variants_including_master,
52
51
  :class_name => 'Variant',
53
- :conditions => ["variants.deleted_at IS NULL"],
52
+ :conditions => ["#{Variant.table_name}.deleted_at IS NULL"],
54
53
  :dependent => :destroy
55
54
 
56
55
  has_many :variants_with_only_master,
57
56
  :class_name => 'Variant',
58
- :conditions => ["variants.deleted_at IS NULL AND variants.is_master = ?", true],
57
+ :conditions => ["#{Variant.table_name}.deleted_at IS NULL AND #{Variant.table_name}.is_master = ?", true],
59
58
  :dependent => :destroy
60
59
 
61
60
 
@@ -74,30 +73,43 @@ class Product < ActiveRecord::Base
74
73
 
75
74
  include ::Scopes::Product
76
75
 
77
- #RAILS3 TODO - scopes are duplicated here and in scopres/product.rb - can we DRY it up?
76
+ #RAILS3 TODO - scopes are duplicated here and in scopes/product.rb - can we DRY it up?
78
77
  # default product scope only lists available and non-deleted products
79
- scope :not_deleted, where("products.deleted_at is NULL")
78
+ class << self
79
+ def not_deleted
80
+ where(Product.arel_table[:deleted_at].eq(nil))
81
+ end
82
+
83
+ def available(available_on = nil)
84
+ where(Product.arel_table[:available_on].lteq(available_on || Time.zone.now ))
85
+ end
80
86
 
81
- scope :available, lambda { |*on| where("products.available_on <= ?", on.first || Time.zone.now ) }
87
+ #RAILS 3 TODO - this scope doesn't match the original 2.3.x version, needs attention (but it works)
88
+ def active
89
+ not_deleted.available
90
+ end
82
91
 
83
- #RAILS 3 TODO - this scope doesn't match the original 2.3.x version, needs attention (but it works)
84
- scope :active, lambda{ not_deleted.available }
92
+ def on_hand
93
+ where(Product.arel_table[:count_on_hand].gteq(0))
94
+ end
85
95
 
86
- scope :on_hand, where("products.count_on_hand > 0")
96
+ def id_equals(input_id)
97
+ where(Product.arel_table[:id].eq(input_id))
98
+ end
87
99
 
100
+ def taxons_name_eq(name)
101
+ joins(:taxons).where(Taxon.arel_table[:name].eq(name))
102
+ end
103
+ end
88
104
  if (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL')
89
- if ActiveRecord::Base.connection.tables.include?("products")
90
- scope :group_by_products_id, { :group => Product.column_names.map{|col_name| "products.#{col_name}"} }
105
+ if Product.table_exists?
106
+ scope :group_by_products_id, { :group => Product.column_names.map{|col_name| "#{Product.table_name}.#{col_name}"} }
91
107
  end
92
108
  else
93
- scope :group_by_products_id, { :group => "products.id" }
109
+ scope :group_by_products_id, { :group => "#{Product.table_name}.id" }
94
110
  end
95
111
  search_methods :group_by_products_id
96
112
 
97
- scope :id_equals, lambda { |input_id| where("products.id = ?", input_id) }
98
-
99
- scope :taxons_name_eq, lambda { |name| joins(:taxons).where("taxons.name = ?", name) }
100
-
101
113
  # ----------------------------------------------------------------------------------------------------------
102
114
  #
103
115
  # The following methods are deprecated and will be removed in a future version of Spree
@@ -135,7 +147,7 @@ class Product < ActiveRecord::Base
135
147
 
136
148
  # returns true if the product has any variants (the master variant is not a member of the variants array)
137
149
  def has_variants?
138
- !variants.empty?
150
+ variants.any?
139
151
  end
140
152
 
141
153
  # returns the number of inventory units "on_hand" for this product
@@ -151,7 +163,7 @@ class Product < ActiveRecord::Base
151
163
 
152
164
  # Returns true if there are inventory units (any variant) with "on_hand" state for this product
153
165
  def has_stock?
154
- master.in_stock? || !!variants.detect{|v| v.in_stock?}
166
+ master.in_stock? || variants.any?(&:in_stock?)
155
167
  end
156
168
 
157
169
  def tax_category
@@ -236,7 +248,7 @@ class Product < ActiveRecord::Base
236
248
  end
237
249
 
238
250
  private
239
-
251
+
240
252
  def sanitize_permalink
241
253
  self.permalink = self.permalink.to_url
242
254
  end