spree_core 2.2.0 → 2.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +26 -0
  3. data/app/helpers/spree/taxons_helper.rb +2 -2
  4. data/app/models/concerns/spree/user_api_authentication.rb +13 -0
  5. data/app/models/concerns/spree/user_reporting.rb +27 -0
  6. data/app/models/spree/adjustment.rb +3 -2
  7. data/app/models/spree/app_configuration.rb +1 -0
  8. data/app/models/spree/calculator/default_tax.rb +5 -1
  9. data/app/models/spree/calculator.rb +2 -2
  10. data/app/models/spree/credit_card.rb +11 -5
  11. data/app/models/spree/gateway.rb +25 -0
  12. data/app/models/spree/item_adjustments.rb +2 -0
  13. data/app/models/spree/legacy_user.rb +2 -3
  14. data/app/models/spree/line_item.rb +7 -0
  15. data/app/models/spree/order/checkout.rb +19 -7
  16. data/app/models/spree/order/currency_updater.rb +40 -0
  17. data/app/models/spree/order.rb +19 -3
  18. data/app/models/spree/order_updater.rb +1 -1
  19. data/app/models/spree/payment.rb +5 -2
  20. data/app/models/spree/payment_method.rb +1 -0
  21. data/app/models/spree/product/scopes.rb +13 -17
  22. data/app/models/spree/product.rb +10 -2
  23. data/app/models/spree/promotion/actions/free_shipping.rb +4 -2
  24. data/app/models/spree/promotion/rules/product.rb +0 -9
  25. data/app/models/spree/promotion.rb +1 -1
  26. data/app/models/spree/shipment.rb +19 -14
  27. data/app/models/spree/shipping_method.rb +2 -2
  28. data/app/models/spree/shipping_rate.rb +15 -9
  29. data/app/models/spree/stock/estimator.rb +4 -1
  30. data/app/models/spree/stock_location.rb +1 -0
  31. data/app/models/spree/tax_rate.rb +81 -7
  32. data/config/initializers/user_class_extensions.rb +3 -0
  33. data/config/locales/en.yml +26 -0
  34. data/db/default/spree/countries.rb +2 -1
  35. data/db/migrate/20130213191427_create_default_stock.rb +3 -3
  36. data/db/migrate/20130417120035_update_adjustment_states.rb +2 -2
  37. data/db/migrate/20130417123427_add_shipping_rates_to_shipments.rb +1 -1
  38. data/db/migrate/20130509115210_add_number_to_stock_transfer.rb +1 -1
  39. data/db/migrate/20130802022321_migrate_tax_categories_to_line_items.rb +5 -7
  40. data/db/migrate/20140307235515_add_user_id_to_spree_credit_cards.rb +13 -0
  41. data/lib/generators/spree/dummy/templates/rails/database.yml +6 -6
  42. data/lib/spree/core/controller_helpers/auth.rb +10 -1
  43. data/lib/spree/core/controller_helpers/order.rb +1 -1
  44. data/lib/spree/core/controller_helpers/respond_with.rb +6 -1
  45. data/lib/spree/core/importer/order.rb +168 -0
  46. data/lib/spree/core/importer.rb +8 -0
  47. data/lib/spree/core/product_duplicator.rb +1 -1
  48. data/lib/spree/core/user_address.rb +2 -0
  49. data/lib/spree/core/user_payment_source.rb +20 -0
  50. data/lib/spree/core/version.rb +1 -1
  51. data/lib/spree/core.rb +4 -0
  52. data/lib/spree/migrations.rb +1 -1
  53. data/lib/spree/permitted_attributes.rb +1 -1
  54. data/lib/spree/testing_support/capybara_ext.rb +23 -1
  55. data/lib/spree/testing_support/factories/credit_card_factory.rb +1 -6
  56. data/lib/spree/testing_support/factories/order_factory.rb +8 -8
  57. data/lib/spree/testing_support/factories/shipping_method_factory.rb +3 -1
  58. data/lib/spree/testing_support/factories/user_factory.rb +5 -0
  59. metadata +11 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bec161c547d1c0a301e068937cff9a461ff7a6cc
4
- data.tar.gz: 5ea11fca1a903043e7776db55c5092a676a31c40
3
+ metadata.gz: d6877ed94eb40aa392d3e75311ae1c46f07d16c4
4
+ data.tar.gz: 86f962251bf0d966fce2dc5562a4e537b05db77c
5
5
  SHA512:
6
- metadata.gz: 1f618e1c470a7a552a2b6b97d72b2702c52d8beba2298d6ab6400982f024a466bc9b857af1e3b447ca5ab8c079d8d44bac78877516baa1a8962ef7f9eac0b244
7
- data.tar.gz: 3be703d2a9d163edb81d227463859a7cb5a181091539780f5b0eb4173ad2f69cf8ff74e824361d312e6571ffbbedef6c8f12eb06fb6649cd2902771f5f537754
6
+ metadata.gz: b9f99321e59660959d319148d1c7deed17c10caedb97e6d29d95dd15c195b2dac43bcfb35d5117c915bc456a1d8c0fb8f2d319971a7bd681f842f77a91ba25b6
7
+ data.tar.gz: 154ee0e0f7d1bd5fa2ce9bce96008951200122a3c687dfe88b6e75284987bef2c12d464c6bba83a2a97747ccd2e9dae7537b2fddc3a4fb9e9cc924c5c11c8491
data/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2007-2014, Spree Commerce, Inc. and other contributors
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+ * Neither the name Spree nor the names of its contributors may be used to
13
+ endorse or promote products derived from this software without specific
14
+ prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
20
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
21
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
22
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
23
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -4,12 +4,12 @@ module Spree
4
4
  # that we can use configurations as well as make it easier for end users to override this determination. One idea is
5
5
  # to show the most popular products for a particular taxon (that is an exercise left to the developer.)
6
6
  def taxon_preview(taxon, max=4)
7
- products = taxon.active_products.limit(max).uniq
7
+ products = taxon.active_products.select("DISTINCT (spree_products.id), spree_products.*, spree_products_taxons.position").limit(max)
8
8
  if (products.size < max)
9
9
  products_arel = Spree::Product.arel_table
10
10
  taxon.descendants.each do |taxon|
11
11
  to_get = max - products.length
12
- products += taxon.active_products.where(products_arel[:id].not_in(products.map(&:id))).limit(to_get).uniq
12
+ products += taxon.active_products.select("DISTINCT (spree_products.id), spree_products.*, spree_products_taxons.position").where(products_arel[:id].not_in(products.map(&:id))).limit(to_get)
13
13
  break if products.size >= max
14
14
  end
15
15
  end
@@ -0,0 +1,13 @@
1
+ module Spree
2
+ module UserApiAuthentication
3
+ def generate_spree_api_key!
4
+ self.spree_api_key = SecureRandom.hex(24)
5
+ save!
6
+ end
7
+
8
+ def clear_spree_api_key!
9
+ self.spree_api_key = nil
10
+ save!
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ module Spree
2
+ module UserReporting
3
+ def lifetime_value
4
+ orders.complete.pluck(:total).sum
5
+ end
6
+
7
+ def display_lifetime_value
8
+ Spree::Money.new(lifetime_value)
9
+ end
10
+
11
+ def order_count
12
+ BigDecimal(orders.complete.count)
13
+ end
14
+
15
+ def average_order_value
16
+ if order_count.to_i > 0
17
+ lifetime_value / order_count
18
+ else
19
+ BigDecimal("0.00")
20
+ end
21
+ end
22
+
23
+ def display_average_order_value
24
+ Spree::Money.new(average_order_value)
25
+ end
26
+ end
27
+ end
@@ -34,14 +34,15 @@ module Spree
34
34
  transition from: :open, to: :closed
35
35
  end
36
36
 
37
- event :close do
38
- transition from: :close, to: :open
37
+ event :open do
38
+ transition from: :closed, to: :open
39
39
  end
40
40
  end
41
41
 
42
42
  after_create :update_adjustable_adjustment_total
43
43
 
44
44
  scope :open, -> { where(state: 'open') }
45
+ scope :closed, -> { where(state: 'closed') }
45
46
  scope :tax, -> { where(source_type: 'Spree::TaxRate') }
46
47
  scope :price, -> { where(adjustable_type: 'Spree::LineItem') }
47
48
  scope :shipping, -> { where(adjustable_type: 'Spree::Shipment') }
@@ -58,6 +58,7 @@ module Spree
58
58
  preference :orders_per_page, :integer, default: 15
59
59
  preference :prices_inc_tax, :boolean, default: false
60
60
  preference :products_per_page, :integer, default: 12
61
+ preference :promotions_per_page, :integer, default: 15
61
62
  preference :redirect_https_to_http, :boolean, :default => false
62
63
  preference :require_master_price, :boolean, default: true
63
64
  preference :shipment_inc_vat, :boolean, default: false
@@ -38,7 +38,11 @@ module Spree
38
38
  def compute_shipping_rate(shipping_rate)
39
39
  if rate.included_in_price
40
40
  pre_tax_amount = shipping_rate.cost / (1 + rate.amount)
41
- deduced_total_by_rate(pre_tax_amount, rate)
41
+ if rate.zone == shipping_rate.shipment.order.tax_zone
42
+ deduced_total_by_rate(pre_tax_amount, rate)
43
+ else
44
+ deduced_total_by_rate(pre_tax_amount, rate) * - 1
45
+ end
42
46
  else
43
47
  with_tax_amount = shipping_rate.cost * rate.amount
44
48
  round_to_two_places(with_tax_amount)
@@ -10,9 +10,9 @@ module Spree
10
10
  computable_name = computable.class.name.demodulize.underscore
11
11
  method = "compute_#{computable_name}".to_sym
12
12
  calculator_class = self.class
13
- begin
13
+ if respond_to?(method)
14
14
  self.send(method, computable)
15
- rescue NoMethodError
15
+ else
16
16
  raise NotImplementedError, "Please implement '#{method}(#{computable_name})' in your calculator: #{calculator_class.name}"
17
17
  end
18
18
  end
@@ -1,15 +1,17 @@
1
1
  module Spree
2
2
  class CreditCard < ActiveRecord::Base
3
+ belongs_to :payment_method
3
4
  has_many :payments, as: :source
4
5
 
5
6
  before_save :set_last_digits
6
7
 
7
- attr_accessor :number, :verification_value
8
+ attr_accessor :number, :verification_value, :encrypted_data
9
+
10
+ validates :month, :year, numericality: { only_integer: true }, if: :require_card_numbers?, on: :create
11
+ validates :number, presence: true, if: :require_card_numbers?, on: :create
12
+ validates :name, presence: true, if: :require_card_numbers?, on: :create
13
+ validates :verification_value, presence: true, if: :require_card_numbers?, on: :create
8
14
 
9
- validates :month, :year, numericality: { only_integer: true }, unless: :has_payment_profile?
10
- validates :number, presence: true, unless: :has_payment_profile?, on: :create
11
- validates :name, presence: true
12
- validates :verification_value, presence: true, unless: :has_payment_profile?, on: :create
13
15
  validate :expiry_not_in_the_past
14
16
 
15
17
  scope :with_payment_profile, -> { where('gateway_customer_profile_id IS NOT NULL') }
@@ -118,5 +120,9 @@ module Spree
118
120
  end
119
121
  end
120
122
  end
123
+
124
+ def require_card_numbers?
125
+ !self.encrypted_data.present? && !self.has_payment_profile?
126
+ end
121
127
  end
122
128
  end
@@ -50,5 +50,30 @@ module Spree
50
50
  return false unless source.brand
51
51
  provider_class.supports?(source.brand)
52
52
  end
53
+
54
+ def disable_customer_profile(source)
55
+ if source.is_a? CreditCard
56
+ source.update_column :gateway_customer_profile_id, nil
57
+ else
58
+ raise 'You must implement disable_customer_profile method for this gateway.'
59
+ end
60
+ end
61
+
62
+ def sources_by_order(order)
63
+ source_ids = order.payments.where(source_type: payment_source_class.to_s, payment_method_id: self.id).pluck(:source_id).uniq
64
+ payment_source_class.where(id: source_ids).with_payment_profile
65
+ end
66
+
67
+ def sources_with_profile(order)
68
+ if order.completed?
69
+ sources_by_order order
70
+ else
71
+ if order.user_id
72
+ self.credit_cards.where(user_id: order.user_id).with_payment_profile
73
+ else
74
+ []
75
+ end
76
+ end
77
+ end
53
78
  end
54
79
  end
@@ -7,6 +7,8 @@ module Spree
7
7
 
8
8
  def initialize(item)
9
9
  @item = item
10
+ # Don't attempt to reload the item from the DB if it's not there
11
+ @item.reload if @item.persisted?
10
12
  end
11
13
 
12
14
  def update
@@ -2,6 +2,7 @@
2
2
  module Spree
3
3
  class LegacyUser < ActiveRecord::Base
4
4
  include Core::UserAddress
5
+ include Core::UserPaymentSource
5
6
 
6
7
  self.table_name = 'spree_users'
7
8
 
@@ -9,8 +10,6 @@ module Spree
9
10
 
10
11
  before_destroy :check_completed_orders
11
12
 
12
- class DestroyWithOrdersError < StandardError; end
13
-
14
13
  def has_spree_role?(role)
15
14
  true
16
15
  end
@@ -21,7 +20,7 @@ module Spree
21
20
  private
22
21
 
23
22
  def check_completed_orders
24
- raise DestroyWithOrdersError if orders.complete.present?
23
+ raise Spree::Core::DestroyWithOrdersError if orders.complete.present?
25
24
  end
26
25
  end
27
26
  end
@@ -22,6 +22,7 @@ module Spree
22
22
  validates :price, numericality: true
23
23
  validates_with Stock::AvailabilityValidator
24
24
 
25
+ validate :ensure_proper_currency
25
26
  before_destroy :update_inventory
26
27
 
27
28
  after_save :update_inventory
@@ -118,6 +119,12 @@ module Spree
118
119
  def create_tax_charge
119
120
  Spree::TaxRate.adjust(order, [self])
120
121
  end
122
+
123
+ def ensure_proper_currency
124
+ unless currency == order.currency
125
+ errors.add(:currency, t(:must_match_order_currency))
126
+ end
127
+ end
121
128
  end
122
129
  end
123
130
 
@@ -81,17 +81,17 @@ module Spree
81
81
  before_transition :from => :address, :do => :create_tax_charge!
82
82
  end
83
83
 
84
+ if states[:payment]
85
+ before_transition :to => :payment, :do => :set_shipments_cost
86
+ before_transition :to => :payment, :do => :create_tax_charge!
87
+ end
88
+
84
89
  if states[:delivery]
85
90
  before_transition :to => :delivery, :do => :create_proposed_shipments
86
91
  before_transition :to => :delivery, :do => :ensure_available_shipping_rates
87
92
  before_transition :from => :delivery, :do => :apply_free_shipping_promotions
88
93
  end
89
94
 
90
- if states[:payment]
91
- before_transition :to => :payment, :do => :set_shipments_cost
92
- before_transition :to => :payment, :do => :create_tax_charge!
93
- end
94
-
95
95
  after_transition :to => :complete, :do => :finalize!
96
96
  after_transition :to => :resumed, :do => :after_resume
97
97
  after_transition :to => :canceled, :do => :after_cancel
@@ -211,6 +211,20 @@ module Spree
211
211
  @updating_params = params
212
212
  run_callbacks :updating_from_params do
213
213
  attributes = @updating_params[:order] ? @updating_params[:order].permit(permitted_params) : {}
214
+
215
+ # Set existing card after setting permitted parameters because
216
+ # rails would slice parameters containg ruby objects, apparently
217
+ if @updating_params[:existing_card].present?
218
+ credit_card = CreditCard.find(@updating_params[:existing_card])
219
+ if credit_card.user_id != self.user_id || credit_card.user_id.blank?
220
+ raise Core::GatewayError.new Spree.t(:invalid_credit_card)
221
+ end
222
+
223
+ attributes[:payments_attributes].first[:source] = credit_card
224
+ attributes[:payments_attributes].first[:payment_method_id] = credit_card.payment_method_id
225
+ attributes[:payments_attributes].first.delete :source_attributes
226
+ end
227
+
214
228
  success = self.update_attributes(attributes)
215
229
  end
216
230
  @updating_params = nil
@@ -222,7 +236,6 @@ module Spree
222
236
  # attributes for a single payment and its source, discarding attributes
223
237
  # for payment methods other than the one selected
224
238
  def update_params_payment_source
225
- # respond_to check is necessary due to issue described in #2910
226
239
  if has_checkout_step?("payment") && self.payment?
227
240
  if @updating_params[:payment_source].present?
228
241
  source_params = @updating_params.delete(:payment_source)[@updating_params[:order][:payments_attributes].first[:payment_method_id].underscore]
@@ -237,7 +250,6 @@ module Spree
237
250
  end
238
251
  end
239
252
  end
240
-
241
253
  end
242
254
  end
243
255
  end
@@ -0,0 +1,40 @@
1
+ module Spree
2
+ class Order < ActiveRecord::Base
3
+ module CurrencyUpdater
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+
8
+ def homogenize_line_item_currencies
9
+ update_line_item_currencies!
10
+ update!
11
+ end
12
+
13
+ end
14
+
15
+ # Updates prices of order's line items
16
+ def update_line_item_currencies!
17
+ line_items.where('currency != ?', currency).each do |line_item|
18
+ update_line_item_price!(line_item)
19
+ end
20
+ end
21
+
22
+ # Returns the price object from given item
23
+ def price_from_line_item(line_item)
24
+ line_item.variant.prices.where(currency: currency).first
25
+ end
26
+
27
+ # Updates price from given line item
28
+ def update_line_item_price!(line_item)
29
+ price = price_from_line_item(line_item)
30
+
31
+ if price
32
+ line_item.update_attributes!(currency: price.currency, price: price.amount)
33
+ else
34
+ raise RuntimeError, "no #{currency} price found for #{line_item.product.name} (#{line_item.variant.sku})"
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -4,13 +4,15 @@ require 'spree/order/checkout'
4
4
  module Spree
5
5
  class Order < ActiveRecord::Base
6
6
  include Checkout
7
+ include CurrencyUpdater
7
8
 
8
9
  checkout_flow do
9
10
  go_to_state :address
10
11
  go_to_state :delivery
11
- go_to_state :payment, if: ->(order) {
12
+ go_to_state :payment, if: ->(order) do
13
+ order.set_shipments_cost if order.shipments.any?
12
14
  order.payment_required?
13
- }
15
+ end
14
16
  go_to_state :confirm, if: ->(order) { order.confirmation_required? }
15
17
  go_to_state :complete
16
18
  remove_transition from: :delivery, to: :confirm
@@ -68,6 +70,7 @@ module Spree
68
70
  attr_accessor :use_billing
69
71
 
70
72
  before_create :link_by_email
73
+ before_update :homogenize_line_item_currencies, if: :currency_changed?
71
74
 
72
75
  validates :email, presence: true, if: :require_email
73
76
  validates :email, email: true, if: :require_email, allow_blank: true
@@ -489,7 +492,7 @@ module Spree
489
492
  # to delivery again so that proper updated shipments are created.
490
493
  # e.g. customer goes back from payment step and changes order items
491
494
  def ensure_updated_shipments
492
- if shipments.any?
495
+ if shipments.any? && !self.completed?
493
496
  self.shipments.destroy_all
494
497
  self.update_column(:shipment_total, 0)
495
498
  restart_checkout_flow
@@ -559,6 +562,19 @@ module Spree
559
562
  update_column(:considered_risky, false)
560
563
  end
561
564
 
565
+ # moved from api order_decorator. This is a better place for it.
566
+ def update_line_items(line_item_params)
567
+ return if line_item_params.blank?
568
+ line_item_params.each_value do |attributes|
569
+ if attributes[:id].present?
570
+ self.line_items.find(attributes[:id]).update_attributes!(attributes)
571
+ else
572
+ self.line_items.create!(attributes)
573
+ end
574
+ end
575
+ self.ensure_updated_shipments
576
+ end
577
+
562
578
  private
563
579
 
564
580
  def link_by_email
@@ -53,7 +53,7 @@ module Spree
53
53
  end
54
54
 
55
55
  def update_shipment_total
56
- order.shipment_total = shipments.sum("cost + promo_total")
56
+ order.shipment_total = shipments.sum(:cost)
57
57
  update_order_total
58
58
  end
59
59
 
@@ -104,7 +104,7 @@ module Spree
104
104
  end
105
105
 
106
106
  def credit_allowed
107
- amount - offsets_total
107
+ amount - offsets_total.abs
108
108
  end
109
109
 
110
110
  def can_credit?
@@ -116,6 +116,8 @@ module Spree
116
116
  return if source_attributes.nil?
117
117
  if payment_method and payment_method.payment_source_class
118
118
  self.source = payment_method.payment_source_class.new(source_attributes)
119
+ self.source.payment_method_id = payment_method.id
120
+ self.source.user_id = self.order.user_id if self.order
119
121
  end
120
122
  end
121
123
 
@@ -159,7 +161,8 @@ module Spree
159
161
  end
160
162
 
161
163
  def create_payment_profile
162
- return unless source.is_a?(CreditCard) && source.number && !source.has_payment_profile?
164
+ return unless source.respond_to?(:has_payment_profile?) && !source.has_payment_profile?
165
+
163
166
  payment_method.create_profile(self)
164
167
  rescue ActiveMerchant::ConnectionError => e
165
168
  gateway_error e
@@ -9,6 +9,7 @@ module Spree
9
9
  validates :name, presence: true
10
10
 
11
11
  has_many :payments, class_name: "Spree::Payment"
12
+ has_many :credit_cards, class_name: "Spree::CreditCard"
12
13
 
13
14
  def self.providers
14
15
  Rails.application.config.spree.payment_methods
@@ -27,6 +27,15 @@ module Spree
27
27
  end
28
28
  end
29
29
 
30
+ def self.property_conditions(property)
31
+ properties = Property.table_name
32
+ conditions = case property
33
+ when String then { "#{properties}.name" => property }
34
+ when Property then { "#{properties}.id" => property.id }
35
+ else { "#{properties}.id" => property.to_i }
36
+ end
37
+ end
38
+
30
39
  add_simple_scopes simple_scopes
31
40
 
32
41
  add_search_scope :ascend_by_master_price do
@@ -83,28 +92,15 @@ module Spree
83
92
 
84
93
  # a scope that finds all products having property specified by name, object or id
85
94
  add_search_scope :with_property do |property|
86
- properties = Property.table_name
87
- conditions = case property
88
- when String then { "#{properties}.name" => property }
89
- when Property then { "#{properties}.id" => property.id }
90
- else { "#{properties}.id" => property.to_i }
91
- end
92
-
93
- joins(:properties).where(conditions)
95
+ joins(:properties).where(property_conditions(property))
94
96
  end
95
97
 
96
98
  # a simple test for product with a certain property-value pairing
97
99
  # note that it can test for properties with NULL values, but not for absent values
98
100
  add_search_scope :with_property_value do |property, value|
99
- properties = Spree::Property.table_name
100
- conditions = case property
101
- when String then ["#{properties}.name = ?", property]
102
- when Property then ["#{properties}.id = ?", property.id]
103
- else ["#{properties}.id = ?", property.to_i]
104
- end
105
- conditions = ["#{ProductProperty.table_name}.value = ? AND #{conditions[0]}", value, conditions[1]]
106
-
107
- joins(:properties).where(conditions)
101
+ joins(:properties)
102
+ .where("#{ProductProperty.table_name}.value = ?", value)
103
+ .where(property_conditions(property))
108
104
  end
109
105
 
110
106
  add_search_scope :with_option do |option|
@@ -78,6 +78,8 @@ module Spree
78
78
  validates :shipping_category_id, presence: true
79
79
  validates :slug, length: { minimum: 3 }
80
80
 
81
+ before_validation :normalize_slug, on: :update
82
+
81
83
  attr_accessor :option_values_hash
82
84
 
83
85
  accepts_nested_attributes_for :product_properties, allow_destroy: true, reject_if: lambda { |pp| pp[:property_name].blank? }
@@ -132,8 +134,11 @@ module Spree
132
134
  !!deleted_at
133
135
  end
134
136
 
137
+ # determine if product is available.
138
+ # deleted products and products with nil or future available_on date
139
+ # are not available
135
140
  def available?
136
- !(available_on.nil? || available_on.future?)
141
+ !(available_on.nil? || available_on.future?) && !deleted?
137
142
  end
138
143
 
139
144
  # split variants list into hash which shows mapping of opt value onto matching variants
@@ -188,7 +193,7 @@ module Spree
188
193
  end
189
194
 
190
195
  def possible_promotions
191
- promotion_ids = promotion_rules.map(&:activator_id).uniq
196
+ promotion_ids = promotion_rules.map(&:promotion_id).uniq
192
197
  Spree::Promotion.advertised.where(id: promotion_ids).reject(&:expired?)
193
198
  end
194
199
 
@@ -208,6 +213,9 @@ module Spree
208
213
  end
209
214
 
210
215
  private
216
+ def normalize_slug
217
+ self.slug = normalize_friendly_id(slug)
218
+ end
211
219
 
212
220
  # Builds variants from a hash of option types & values
213
221
  def build_variants_from_option_values_hash
@@ -4,8 +4,6 @@ module Spree
4
4
  class FreeShipping < Spree::PromotionAction
5
5
  def perform(payload={})
6
6
  order = payload[:order]
7
-
8
- label = "#{Spree.t(:promotion)} (#{promotion.name})"
9
7
  results = order.shipments.map do |shipment|
10
8
  return false if promotion_credit_exists?(shipment)
11
9
  shipment.adjustments.create!(
@@ -21,6 +19,10 @@ module Spree
21
19
  results.any? { |r| r == true }
22
20
  end
23
21
 
22
+ def label
23
+ "#{Spree.t(:promotion)} (#{promotion.name})"
24
+ end
25
+
24
26
  def compute_amount(shipment)
25
27
  shipment.cost * -1
26
28
  end
@@ -6,7 +6,6 @@ module Spree
6
6
  module Rules
7
7
  class Product < PromotionRule
8
8
  has_and_belongs_to_many :products, class_name: '::Spree::Product', join_table: 'spree_products_promotion_rules', foreign_key: 'promotion_rule_id'
9
- validate :only_one_promotion_per_product
10
9
 
11
10
  MATCH_POLICIES = %w(any all)
12
11
  preference :match_policy, :string, default: MATCH_POLICIES.first
@@ -36,14 +35,6 @@ module Spree
36
35
  def product_ids_string=(s)
37
36
  self.product_ids = s.to_s.split(',').map(&:strip)
38
37
  end
39
-
40
- private
41
-
42
- def only_one_promotion_per_product
43
- if Spree::Promotion::Rules::Product.all.map(&:products).flatten.uniq!
44
- errors[:base] << "You can't create two promotions for the same product"
45
- end
46
- end
47
38
  end
48
39
  end
49
40
  end
@@ -40,7 +40,7 @@ module Spree
40
40
  end
41
41
 
42
42
  def expired?
43
- starts_at && Time.now < starts_at || expires_at && Time.now > expires_at
43
+ !!(starts_at && Time.now < starts_at || expires_at && Time.now > expires_at)
44
44
  end
45
45
 
46
46
  def activate(payload)