spree_core 0.70.RC1 → 0.70.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
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