spree_core 2.1.3 → 2.1.4

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/app/helpers/spree/admin/images_helper.rb +1 -1
  3. data/app/helpers/spree/base_helper.rb +5 -2
  4. data/app/models/spree/address.rb +9 -1
  5. data/app/models/spree/adjustment.rb +2 -2
  6. data/app/models/spree/calculator/default_tax.rb +5 -1
  7. data/app/models/spree/credit_card.rb +2 -0
  8. data/app/models/spree/gateway.rb +1 -1
  9. data/app/models/spree/inventory_unit.rb +5 -4
  10. data/app/models/spree/legacy_user.rb +2 -11
  11. data/app/models/spree/line_item.rb +2 -3
  12. data/app/models/spree/log_entry.rb +4 -0
  13. data/app/models/spree/option_type.rb +6 -0
  14. data/app/models/spree/option_value.rb +12 -1
  15. data/app/models/spree/order.rb +34 -16
  16. data/app/models/spree/order/checkout.rb +4 -0
  17. data/app/models/spree/order_inventory.rb +1 -1
  18. data/app/models/spree/payment.rb +10 -2
  19. data/app/models/spree/payment/processing.rb +5 -4
  20. data/app/models/spree/payment_method.rb +2 -0
  21. data/app/models/spree/price.rb +5 -0
  22. data/app/models/spree/product.rb +6 -5
  23. data/app/models/spree/product/scopes.rb +12 -6
  24. data/app/models/spree/product_property.rb +1 -1
  25. data/app/models/spree/promotion.rb +1 -8
  26. data/app/models/spree/promotion/rules/user_logged_in.rb +1 -3
  27. data/app/models/spree/property.rb +8 -0
  28. data/app/models/spree/shipment.rb +9 -14
  29. data/app/models/spree/shipping_method.rb +3 -2
  30. data/app/models/spree/shipping_rate.rb +7 -9
  31. data/app/models/spree/stock/estimator.rb +21 -14
  32. data/app/models/spree/stock/package.rb +1 -1
  33. data/app/models/spree/stock/packer.rb +1 -1
  34. data/app/models/spree/stock/quantifier.rb +11 -2
  35. data/app/models/spree/stock_item.rb +2 -2
  36. data/app/models/spree/stock_location.rb +8 -0
  37. data/app/models/spree/stock_movement.rb +3 -1
  38. data/app/models/spree/taxon.rb +2 -2
  39. data/app/models/spree/variant.rb +19 -4
  40. data/app/models/spree/zone.rb +1 -1
  41. data/app/views/spree/shared/_routes.html.erb +1 -1
  42. data/config/locales/en.yml +15 -1
  43. data/db/default/spree/countries.rb +7 -7
  44. data/db/migrate/20130417120034_add_index_to_source_columns_on_adjustments.rb +5 -0
  45. data/db/migrate/20130802022321_migrate_tax_categories_to_line_items.rb +5 -2
  46. data/db/migrate/20131026154747_add_track_inventory_to_variant.rb +5 -0
  47. data/db/migrate/20131120234456_add_updated_at_to_variants.rb +5 -0
  48. data/db/migrate/20131211192741_unique_shipping_method_categories.rb +24 -0
  49. data/db/migrate/20140120160805_add_index_to_variant_id_and_currency_on_prices.rb +5 -0
  50. data/lib/generators/spree/dummy/dummy_generator.rb +14 -3
  51. data/lib/generators/spree/dummy/templates/rails/database.yml +10 -0
  52. data/lib/spree/core.rb +3 -0
  53. data/lib/spree/core/controller_helpers/order.rb +4 -1
  54. data/lib/spree/core/controller_helpers/ssl.rb +5 -7
  55. data/lib/spree/core/controller_helpers/strong_parameters.rb +6 -0
  56. data/lib/spree/core/delegate_belongs_to.rb +16 -10
  57. data/lib/spree/core/engine.rb +11 -2
  58. data/lib/spree/core/mail_method.rb +27 -0
  59. data/lib/spree/core/mail_settings.rb +33 -38
  60. data/lib/spree/core/permalinks.rb +5 -1
  61. data/lib/spree/core/s3_support.rb +1 -1
  62. data/lib/spree/core/user_address.rb +30 -0
  63. data/lib/spree/core/validators/email.rb +9 -3
  64. data/lib/spree/core/version.rb +1 -1
  65. data/lib/spree/i18n.rb +1 -0
  66. data/lib/spree/migrations.rb +55 -0
  67. data/lib/spree/money.rb +171 -1
  68. data/lib/spree/permitted_attributes.rb +6 -6
  69. data/lib/spree/testing_support/capybara_ext.rb +6 -5
  70. data/lib/spree/testing_support/controller_requests.rb +20 -4
  71. data/lib/spree/testing_support/factories/product_factory.rb +4 -0
  72. data/lib/spree/testing_support/factories/variant_factory.rb +15 -0
  73. metadata +158 -164
@@ -23,7 +23,7 @@ module Spree
23
23
  # We should not define price scopes here, as they require something slightly different
24
24
  next if name.to_s.include?("master_price")
25
25
  parts = name.to_s.match(/(.*)_by_(.*)/)
26
- self.scope(name.to_s, -> { relation.order("#{Product.quoted_table_name}.#{parts[2]} #{parts[1] == 'ascend' ? "ASC" : "DESC"}") })
26
+ self.scope(name.to_s, -> { order("#{Product.quoted_table_name}.#{parts[2]} #{parts[1] == 'ascend' ? "ASC" : "DESC"}") })
27
27
  end
28
28
  end
29
29
 
@@ -207,13 +207,19 @@ module Spree
207
207
  group("spree_products.id").joins(:taxons).where(Taxon.arel_table[:name].eq(name))
208
208
  end
209
209
 
210
- # This method needs to be defined *as a method*, otherwise it will cause the
211
- # problem shown in #1247.
212
- def self.group_by_products_id
210
+ def self.distinct_by_product_ids(sort_order=nil)
213
211
  if (ActiveRecord::Base.connection.adapter_name == 'PostgreSQL')
214
- group(column_names.map { |col_name| "#{table_name}.#{col_name}"})
212
+ sort_column = sort_order.split(" ").first
213
+ # Don't allow sort_column, a variable coming from params,
214
+ # to be anything but a column in the database
215
+ if column_names.include?(sort_column)
216
+ distinct_fields = ["id", sort_column].compact.join(",")
217
+ select("DISTINCT ON(#{distinct_fields}) spree_products.*")
218
+ else
219
+ scoped
220
+ end
215
221
  else
216
- group("#{self.quoted_table_name}.id")
222
+ select("DISTINCT spree_products.*")
217
223
  end
218
224
  end
219
225
 
@@ -1,6 +1,6 @@
1
1
  module Spree
2
2
  class ProductProperty < ActiveRecord::Base
3
- belongs_to :product, class_name: 'Spree::Product'
3
+ belongs_to :product, touch: true, class_name: 'Spree::Product'
4
4
  belongs_to :property, class_name: 'Spree::Property'
5
5
 
6
6
  validates :property, presence: true
@@ -21,13 +21,6 @@ module Spree
21
21
  validates :path, presence: true, if: lambda{|r| r.event_name == 'spree.content.visited' }
22
22
  validates :usage_limit, numericality: { greater_than: 0, allow_nil: true }
23
23
 
24
- # TODO: This shouldn't be necessary with :autosave option but nested attribute updating of actions is broken without it
25
- after_save :save_rules_and_actions
26
-
27
- def save_rules_and_actions
28
- (rules + actions).each &:save
29
- end
30
-
31
24
  def self.advertised
32
25
  where(advertise: true)
33
26
  end
@@ -91,7 +84,7 @@ module Spree
91
84
  end
92
85
 
93
86
  def credits
94
- Adjustment.promotion.where(originator_id: actions.map(&:id))
87
+ Adjustment.eligible.promotion.where(originator_id: actions.map(&:id))
95
88
  end
96
89
 
97
90
  def credits_count
@@ -2,11 +2,9 @@ module Spree
2
2
  class Promotion
3
3
  module Rules
4
4
  class UserLoggedIn < PromotionRule
5
-
6
5
  def eligible?(order, options = {})
7
- return order.try(:user).try(:anonymous?) == false
6
+ return order.user.present?
8
7
  end
9
-
10
8
  end
11
9
  end
12
10
  end
@@ -9,11 +9,19 @@ module Spree
9
9
 
10
10
  scope :sorted, -> { order(:name) }
11
11
 
12
+ after_touch :touch_all_products
13
+
12
14
  def self.find_all_by_prototype(prototype)
13
15
  id = prototype
14
16
  id = prototype.id if prototype.class == Prototype
15
17
  joins("LEFT JOIN properties_prototypes ON property_id = #{self.table_name}.id").
16
18
  where(prototype_id: id)
17
19
  end
20
+
21
+ private
22
+
23
+ def touch_all_products
24
+ products.each(&:touch)
25
+ end
18
26
  end
19
27
  end
@@ -2,7 +2,7 @@ require 'ostruct'
2
2
 
3
3
  module Spree
4
4
  class Shipment < ActiveRecord::Base
5
- belongs_to :order, class_name: 'Spree::Order'
5
+ belongs_to :order, class_name: 'Spree::Order', touch: true
6
6
  belongs_to :address, class_name: 'Spree::Address'
7
7
  belongs_to :stock_location, class_name: 'Spree::StockLocation'
8
8
 
@@ -12,7 +12,6 @@ module Spree
12
12
  has_many :inventory_units, dependent: :delete_all
13
13
  has_one :adjustment, as: :source, dependent: :destroy
14
14
 
15
- before_create :generate_shipment_number
16
15
  after_save :ensure_correct_adjustment, :update_order
17
16
 
18
17
  attr_accessor :special_instructions
@@ -20,7 +19,7 @@ module Spree
20
19
  accepts_nested_attributes_for :address
21
20
  accepts_nested_attributes_for :inventory_units
22
21
 
23
- make_permalink field: :number
22
+ make_permalink field: :number, length: 11, prefix: 'H'
24
23
 
25
24
  scope :shipped, -> { with_state('shipped') }
26
25
  scope :ready, -> { with_state('ready') }
@@ -164,8 +163,8 @@ module Spree
164
163
  end
165
164
 
166
165
  def line_items
167
- if order.complete? and Spree::Config[:track_inventory_levels]
168
- order.line_items.select { |li| inventory_units.pluck(:variant_id).include?(li.variant_id) }
166
+ if order.complete? and Spree::Config.track_inventory_levels
167
+ order.line_items.select { |li| !li.should_track_inventory? || inventory_units.pluck(:variant_id).include?(li.variant_id) }
169
168
  else
170
169
  order.line_items
171
170
  end
@@ -238,17 +237,13 @@ module Spree
238
237
  end
239
238
 
240
239
  def manifest_restock(item)
241
- stock_location.restock item.variant, item.quantity, self
242
- end
240
+ if item.states["on_hand"].to_i > 0
241
+ stock_location.restock item.variant, item.states["on_hand"], self
242
+ end
243
243
 
244
- def generate_shipment_number
245
- return number unless number.blank?
246
- record = true
247
- while record
248
- random = "H#{Array.new(11) { rand(9) }.join}"
249
- record = self.class.where(number: random).first
244
+ if item.states["backordered"].to_i > 0
245
+ stock_location.restock_backordered item.variant, item.states["backordered"]
250
246
  end
251
- self.number = random
252
247
  end
253
248
 
254
249
  def description_for_shipping_charge
@@ -8,7 +8,7 @@ module Spree
8
8
  has_many :shipments
9
9
  has_many :shipping_method_categories
10
10
  has_many :shipping_categories, through: :shipping_method_categories
11
- has_many :shipping_rates
11
+ has_many :shipping_rates, inverse_of: :shipping_method
12
12
 
13
13
  has_and_belongs_to_many :zones, :join_table => 'spree_shipping_methods_zones',
14
14
  :class_name => 'Spree::Zone',
@@ -30,7 +30,8 @@ module Spree
30
30
  end
31
31
 
32
32
  def build_tracking_url(tracking)
33
- tracking_url.gsub(/:tracking/, tracking) unless tracking.blank? || tracking_url.blank?
33
+ return if tracking.blank? || tracking_url.blank?
34
+ tracking_url.gsub(/:tracking/, ERB::Util.url_encode(tracking)) # :url_encode exists in 1.8.7 through 2.1.0
34
35
  end
35
36
 
36
37
  def self.calculators
@@ -1,19 +1,13 @@
1
1
  module Spree
2
2
  class ShippingRate < ActiveRecord::Base
3
3
  belongs_to :shipment, class_name: 'Spree::Shipment'
4
- belongs_to :shipping_method, class_name: 'Spree::ShippingMethod'
4
+ belongs_to :shipping_method, class_name: 'Spree::ShippingMethod', inverse_of: :shipping_rates
5
5
 
6
- scope :frontend,
6
+ scope :with_shipping_method,
7
7
  -> { includes(:shipping_method).
8
- where(ShippingMethod.on_frontend_query).
9
- references(:shipping_method).
10
- order("cost ASC") }
11
- scope :backend,
12
- -> { includes(:shipping_method).
13
- where(ShippingMethod.on_backend_query).
14
8
  references(:shipping_method).
15
9
  order("cost ASC") }
16
-
10
+
17
11
  delegate :order, :currency, to: :shipment
18
12
  delegate :name, to: :shipping_method
19
13
 
@@ -28,5 +22,9 @@ module Spree
28
22
  end
29
23
 
30
24
  alias_method :display_cost, :display_price
25
+
26
+ def shipping_method
27
+ Spree::ShippingMethod.unscoped { super }
28
+ end
31
29
  end
32
30
  end
@@ -9,20 +9,17 @@ module Spree
9
9
  end
10
10
 
11
11
  def shipping_rates(package, frontend_only = true)
12
- shipping_rates = calculate_shipping_rates(package)
13
-
14
- unless shipping_rates.empty?
15
- available_rates = shipping_rates.clone
16
- available_rates.select! { |rate| rate.shipping_method.frontend? } if frontend_only
17
- choose_default_shipping_rate(available_rates)
18
- end
19
-
20
- sort_shipping_rates(shipping_rates)
12
+ rates = calculate_shipping_rates(package)
13
+ rates.select! { |rate| rate.shipping_method.frontend? } if frontend_only
14
+ choose_default_shipping_rate(rates)
15
+ sort_shipping_rates(rates)
21
16
  end
22
17
 
23
18
  private
24
19
  def choose_default_shipping_rate(shipping_rates)
25
- shipping_rates.min_by(&:cost).selected = true
20
+ unless shipping_rates.empty?
21
+ shipping_rates.min_by(&:cost).selected = true
22
+ end
26
23
  end
27
24
 
28
25
  def sort_shipping_rates(shipping_rates)
@@ -39,12 +36,22 @@ module Spree
39
36
  def shipping_methods(package)
40
37
  package.shipping_methods.select do |ship_method|
41
38
  calculator = ship_method.calculator
42
- calculator.available?(package) &&
43
- ship_method.include?(order.ship_address) &&
44
- (calculator.preferences[:currency].nil? ||
45
- calculator.preferences[:currency] == currency)
39
+ begin
40
+ calculator.available?(package) &&
41
+ ship_method.include?(order.ship_address) &&
42
+ (calculator.preferences[:currency].nil? ||
43
+ calculator.preferences[:currency] == currency)
44
+ rescue Exception => exception
45
+ log_calculator_exception(ship_method, exception)
46
+ end
46
47
  end
47
48
  end
49
+
50
+ def log_calculator_exception(ship_method, exception)
51
+ Rails.logger.info("Something went wrong calculating rates with the #{ship_method.name} (ID=#{ship_method.id}) shipping method.")
52
+ Rails.logger.info("*" * 50)
53
+ Rails.logger.info(exception.backtrace.join("\n"))
54
+ end
48
55
  end
49
56
  end
50
57
  end
@@ -82,7 +82,7 @@ module Spree
82
82
  end
83
83
 
84
84
  def shipping_methods
85
- shipping_categories.map { |sc| sc.shipping_methods }.flatten.uniq
85
+ shipping_categories.map(&:shipping_methods).reduce(:&).to_a
86
86
  end
87
87
 
88
88
  def inspect
@@ -20,7 +20,7 @@ module Spree
20
20
  def default_package
21
21
  package = Package.new(stock_location, order)
22
22
  order.line_items.each do |line_item|
23
- if Config.track_inventory_levels
23
+ if line_item.should_track_inventory?
24
24
  next unless stock_location.stock_item(line_item.variant)
25
25
 
26
26
  on_hand, backordered = stock_location.fill_status(line_item.variant, line_item.quantity)
@@ -4,12 +4,12 @@ module Spree
4
4
  attr_reader :stock_items
5
5
 
6
6
  def initialize(variant)
7
- @variant = variant
7
+ @variant = resolve_variant_id(variant)
8
8
  @stock_items = Spree::StockItem.joins(:stock_location).where(:variant_id => @variant, Spree::StockLocation.table_name =>{ :active => true})
9
9
  end
10
10
 
11
11
  def total_on_hand
12
- if Spree::Config.track_inventory_levels
12
+ if @variant.should_track_inventory?
13
13
  stock_items.sum(:count_on_hand)
14
14
  else
15
15
  Float::INFINITY
@@ -23,6 +23,15 @@ module Spree
23
23
  def can_supply?(required)
24
24
  total_on_hand >= required || backorderable?
25
25
  end
26
+
27
+ private
28
+
29
+ # return variant when passed either variant object or variant id
30
+ def resolve_variant_id(variant)
31
+ variant = Spree::Variant.find_by_id(variant) unless variant.respond_to?(:should_track_inventory?)
32
+ variant
33
+ end
34
+
26
35
  end
27
36
  end
28
37
  end
@@ -3,13 +3,13 @@ module Spree
3
3
  acts_as_paranoid
4
4
 
5
5
  belongs_to :stock_location, class_name: 'Spree::StockLocation'
6
- belongs_to :variant, class_name: 'Spree::Variant'
6
+ belongs_to :variant, class_name: 'Spree::Variant', touch: true
7
7
  has_many :stock_movements
8
8
 
9
9
  validates_presence_of :stock_location, :variant
10
10
  validates_uniqueness_of :variant_id, scope: [:stock_location_id, :deleted_at]
11
11
 
12
- delegate :weight, to: :variant
12
+ delegate :weight, :should_track_inventory?, to: :variant
13
13
 
14
14
  def backordered_inventory_units
15
15
  Spree::InventoryUnit.backordered_for_stock_item(self)
@@ -44,6 +44,14 @@ module Spree
44
44
  move(variant, quantity, originator)
45
45
  end
46
46
 
47
+ def restock_backordered(variant, quantity, originator = nil)
48
+ item = stock_item_or_create(variant)
49
+ item.update_columns(
50
+ count_on_hand: item.count_on_hand + quantity,
51
+ updated_at: Time.now
52
+ )
53
+ end
54
+
47
55
  def unstock(variant, quantity, originator = nil)
48
56
  move(variant, -quantity, originator)
49
57
  end
@@ -16,10 +16,12 @@ module Spree
16
16
  end
17
17
 
18
18
  private
19
+
19
20
  def update_stock_item_quantity
20
- return unless Spree::Config[:track_inventory_levels]
21
+ return unless self.stock_item.should_track_inventory?
21
22
  stock_item.adjust_count_on_hand quantity
22
23
  end
24
+
23
25
  end
24
26
  end
25
27
 
@@ -16,7 +16,7 @@ module Spree
16
16
  url: '/spree/taxons/:id/:style/:basename.:extension',
17
17
  path: ':rails_root/public/spree/taxons/:id/:style/:basename.:extension',
18
18
  default_url: '/assets/default_taxon.png'
19
-
19
+
20
20
  include Spree::Core::S3Support
21
21
  supports_s3 :icon
22
22
 
@@ -76,7 +76,7 @@ module Spree
76
76
  #
77
77
  # See #3390 for background.
78
78
  def child_index=(idx)
79
- move_to_child_with_index(parent, idx.to_i)
79
+ move_to_child_with_index(parent, idx.to_i) unless self.new_record?
80
80
  end
81
81
  end
82
82
  end
@@ -23,7 +23,7 @@ module Spree
23
23
  class_name: 'Spree::Price',
24
24
  dependent: :destroy
25
25
 
26
- delegate_belongs_to :default_price, :display_price, :display_amount, :price, :price=, :currency
26
+ delegate_belongs_to :default_price, :display_price, :display_amount, :price, :price=, :currency
27
27
 
28
28
  has_many :prices,
29
29
  class_name: 'Spree::Price',
@@ -40,7 +40,7 @@ module Spree
40
40
  after_create :set_position
41
41
 
42
42
  # default variant scope only lists non-deleted variants
43
- scope :deleted, lambda { where('deleted_at IS NOT NULL') }
43
+ scope :deleted, lambda { where.not(deleted_at: nil) }
44
44
 
45
45
  def self.active(currency = nil)
46
46
  joins(:prices).where(deleted_at: nil).where('spree_prices.currency' => currency || Spree::Config[:currency]).where('spree_prices.amount IS NOT NULL')
@@ -76,6 +76,12 @@ module Spree
76
76
  deleted_at
77
77
  end
78
78
 
79
+ def options=(options = {})
80
+ options.each do |option|
81
+ set_option_value(option[:name], option[:value])
82
+ end
83
+ end
84
+
79
85
  def set_option_value(opt_name, opt_value)
80
86
  # no option values on master
81
87
  return if self.is_master
@@ -94,7 +100,6 @@ module Spree
94
100
  # then we have to check to make sure that the product has the option type
95
101
  unless self.product.option_types.include? option_type
96
102
  self.product.option_types << option_type
97
- self.product.save
98
103
  end
99
104
  end
100
105
 
@@ -127,13 +132,17 @@ module Spree
127
132
  "#{name} - #{sku}"
128
133
  end
129
134
 
135
+ def sku_and_options_text
136
+ "#{sku} #{options_text}".strip
137
+ end
138
+
130
139
  # Product may be created with deleted_at already set,
131
140
  # which would make AR's default finder return nil.
132
141
  # This is a stopgap for that little problem.
133
142
  def product
134
143
  Spree::Product.unscoped { super }
135
144
  end
136
-
145
+
137
146
  def in_stock?(quantity=1)
138
147
  Spree::Stock::Quantifier.new(self).can_supply?(quantity)
139
148
  end
@@ -142,6 +151,12 @@ module Spree
142
151
  Spree::Stock::Quantifier.new(self).total_on_hand
143
152
  end
144
153
 
154
+ # Shortcut method to determine if inventory tracking is enabled for this variant
155
+ # This considers both variant tracking flag and site-wide inventory tracking settings
156
+ def should_track_inventory?
157
+ self.track_inventory? && Spree::Config.track_inventory_levels
158
+ end
159
+
145
160
  private
146
161
  # strips all non-price-like characters from the price, taking into account locale settings
147
162
  def parse_price(price)