spree_core 2.3.1 → 2.3.2

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/app/helpers/spree/base_helper.rb +3 -3
  3. data/app/models/spree/ability.rb +1 -0
  4. data/app/models/spree/app_configuration.rb +0 -1
  5. data/app/models/spree/base.rb +6 -0
  6. data/app/models/spree/calculator/flat_percent_item_total.rb +9 -3
  7. data/app/models/spree/calculator/flexi_rate.rb +1 -1
  8. data/app/models/spree/calculator/percent_on_line_item.rb +1 -1
  9. data/app/models/spree/calculator/tiered_flat_rate.rb +37 -0
  10. data/app/models/spree/calculator/tiered_percent.rb +44 -0
  11. data/app/models/spree/credit_card.rb +35 -14
  12. data/app/models/spree/inventory_unit.rb +1 -0
  13. data/app/models/spree/item_adjustments.rb +3 -2
  14. data/app/models/spree/line_item.rb +2 -2
  15. data/app/models/spree/order.rb +36 -20
  16. data/app/models/spree/order/checkout.rb +60 -24
  17. data/app/models/spree/order_contents.rb +3 -6
  18. data/app/models/spree/order_populator.rb +1 -1
  19. data/app/models/spree/order_updater.rb +19 -4
  20. data/app/models/spree/payment.rb +4 -0
  21. data/app/models/spree/payment/processing.rb +6 -2
  22. data/app/models/spree/price.rb +10 -0
  23. data/app/models/spree/product.rb +81 -54
  24. data/app/models/spree/promotion/actions/create_adjustment.rb +2 -11
  25. data/app/models/spree/promotion/actions/create_item_adjustments.rb +2 -19
  26. data/app/models/spree/promotion_handler/cart.rb +14 -2
  27. data/app/models/spree/promotion_handler/coupon.rb +8 -2
  28. data/app/models/spree/return_authorization.rb +2 -2
  29. data/app/models/spree/shipping_rate.rb +2 -2
  30. data/app/models/spree/stock/availability_validator.rb +3 -7
  31. data/app/models/spree/stock/estimator.rb +1 -1
  32. data/app/models/spree/stock/package.rb +1 -0
  33. data/app/models/spree/stock_item.rb +6 -1
  34. data/app/models/spree/stock_location.rb +4 -0
  35. data/app/models/spree/tax_rate.rb +15 -2
  36. data/app/models/spree/variant.rb +8 -3
  37. data/app/models/spree/zone.rb +2 -2
  38. data/config/locales/en.yml +33 -3
  39. data/db/default/spree/countries.rb +2 -1
  40. data/db/migrate/20130807024302_rename_adjustment_fields.rb +2 -5
  41. data/db/migrate/20140804185157_add_default_to_shipment_cost.rb +10 -0
  42. data/lib/generators/spree/custom_user/templates/authentication_helpers.rb.tt +12 -4
  43. data/lib/generators/spree/install/install_generator.rb +8 -0
  44. data/lib/spree/core.rb +1 -0
  45. data/lib/spree/core/adjustment_source.rb +26 -0
  46. data/lib/spree/core/controller_helpers.rb +10 -9
  47. data/lib/spree/core/controller_helpers/order.rb +18 -5
  48. data/lib/spree/core/engine.rb +6 -2
  49. data/lib/spree/core/importer/order.rb +52 -9
  50. data/lib/spree/core/version.rb +1 -1
  51. data/lib/spree/permitted_attributes.rb +4 -4
  52. data/lib/spree/testing_support/authorization_helpers.rb +1 -1
  53. data/lib/spree/testing_support/factories/product_factory.rb +1 -1
  54. metadata +27 -37
@@ -9,6 +9,7 @@ module Spree
9
9
  def add(variant, quantity = 1, currency = nil, shipment = nil)
10
10
  line_item = add_to_line_item(variant, quantity, currency, shipment)
11
11
  reload_totals
12
+ shipment.present? ? shipment.update_amounts : order.ensure_updated_shipments
12
13
  PromotionHandler::Cart.new(order, line_item).activate
13
14
  ItemAdjustments.new(line_item).update
14
15
  reload_totals
@@ -18,6 +19,7 @@ module Spree
18
19
  def remove(variant, quantity = 1, shipment = nil)
19
20
  line_item = remove_from_line_item(variant, quantity, shipment)
20
21
  reload_totals
22
+ shipment.present? ? shipment.update_amounts : order.ensure_updated_shipments
21
23
  PromotionHandler::Cart.new(order, line_item).activate
22
24
  ItemAdjustments.new(line_item).update
23
25
  reload_totals
@@ -47,12 +49,7 @@ module Spree
47
49
 
48
50
  def reload_totals
49
51
  order_updater.update_item_count
50
- order_updater.update_item_total
51
- order_updater.update_adjustment_total
52
-
53
- order_updater.update_payment_state if order.completed?
54
- order_updater.persist_totals
55
-
52
+ order_updater.update
56
53
  order.reload
57
54
  end
58
55
 
@@ -9,9 +9,9 @@ module Spree
9
9
  @errors = ActiveModel::Errors.new(self)
10
10
  end
11
11
 
12
-
13
12
  def populate(variant_id, quantity)
14
13
  attempt_cart_add(variant_id, quantity)
14
+ order.ensure_updated_shipments
15
15
  valid?
16
16
  end
17
17
 
@@ -1,7 +1,7 @@
1
1
  module Spree
2
2
  class OrderUpdater
3
3
  attr_reader :order
4
- delegate :payments, :line_items, :adjustments, :all_adjustments, :shipments, :update_hooks, to: :order
4
+ delegate :payments, :line_items, :adjustments, :all_adjustments, :shipments, :update_hooks, :quantity, to: :order
5
5
 
6
6
  def initialize(order)
7
7
  @order = order
@@ -38,6 +38,7 @@ module Spree
38
38
  # +payment_total+ The total value of all finalized Payments (NOTE: non-finalized Payments are excluded)
39
39
  # +item_total+ The total value of all LineItems
40
40
  # +adjustment_total+ The total value of all adjustments (promotions, credits, etc.)
41
+ # +promo_total+ The total value of all promotion adjustments
41
42
  # +total+ The so-called "order total." This is equivalent to +item_total+ plus +adjustment_total+.
42
43
  def update_totals
43
44
  update_payment_total
@@ -49,7 +50,12 @@ module Spree
49
50
 
50
51
  # give each of the shipments a chance to update themselves
51
52
  def update_shipments
52
- shipments.each { |shipment| shipment.update!(order) }
53
+ shipments.each do |shipment|
54
+ next unless shipment.persisted?
55
+ shipment.update!(order)
56
+ shipment.refresh_rates
57
+ shipment.update_amounts
58
+ end
53
59
  end
54
60
 
55
61
  def update_payment_total
@@ -73,11 +79,15 @@ module Spree
73
79
  order.included_tax_total = line_items.sum(:included_tax_total) + shipments.sum(:included_tax_total)
74
80
  order.additional_tax_total = line_items.sum(:additional_tax_total) + shipments.sum(:additional_tax_total)
75
81
 
82
+ order.promo_total = line_items.sum(:promo_total) +
83
+ shipments.sum(:promo_total) +
84
+ adjustments.promotion.eligible.sum(:amount)
85
+
76
86
  update_order_total
77
87
  end
78
88
 
79
89
  def update_item_count
80
- order.item_count = line_items.sum(:quantity)
90
+ order.item_count = quantity
81
91
  end
82
92
 
83
93
  def update_item_total
@@ -96,6 +106,7 @@ module Spree
96
106
  additional_tax_total: order.additional_tax_total,
97
107
  payment_total: order.payment_total,
98
108
  shipment_total: order.shipment_total,
109
+ promo_total: order.promo_total,
99
110
  total: order.total,
100
111
  updated_at: Time.now,
101
112
  )
@@ -145,8 +156,12 @@ module Spree
145
156
  # The +payment_state+ value helps with reporting, etc. since it provides a quick and easy way to locate Orders needing attention.
146
157
  def update_payment_state
147
158
  last_state = order.payment_state
148
- if payments.present? && payments.last.state == 'failed'
159
+ if payments.present? && payments.valid.size == 0
149
160
  order.payment_state = 'failed'
161
+ elsif !payments.present? && order.state == 'canceled'
162
+ order.payment_state = 'void'
163
+ elsif order.state == 'canceled' && order.payment_total == 0 && payments.completed.size > 0
164
+ order.payment_state = 'void'
150
165
  else
151
166
  order.payment_state = 'balance_due' if order.outstanding_balance > 0
152
167
  order.payment_state = 'credit_owed' if order.outstanding_balance < 0
@@ -30,6 +30,8 @@ module Spree
30
30
 
31
31
  after_initialize :build_source
32
32
 
33
+ default_scope -> { order("#{self.table_name}.created_at") }
34
+
33
35
  scope :from_credit_card, -> { where(source_type: 'Spree::CreditCard') }
34
36
  scope :with_state, ->(s) { where(state: s.to_s) }
35
37
  scope :completed, -> { with_state('completed') }
@@ -173,6 +175,8 @@ module Spree
173
175
 
174
176
  def create_payment_profile
175
177
  return unless source.respond_to?(:has_payment_profile?) && !source.has_payment_profile?
178
+ # Imported payments shouldn't create a payment profile.
179
+ return if source.imported
176
180
 
177
181
  payment_method.create_profile(self)
178
182
  rescue ActiveMerchant::ConnectionError => e
@@ -5,7 +5,7 @@ module Spree
5
5
  if payment_method && payment_method.source_required?
6
6
  if source
7
7
  if !processing?
8
- if payment_method.supports?(source)
8
+ if payment_method.supports?(source) || token_based?
9
9
  if payment_method.auto_capture?
10
10
  purchase!
11
11
  else
@@ -115,7 +115,7 @@ module Spree
115
115
  if payment_method.respond_to?(:cancel)
116
116
  payment_method.cancel(response_code)
117
117
  else
118
- credit!
118
+ credit!(credit_allowed.abs)
119
119
  end
120
120
  end
121
121
 
@@ -219,6 +219,10 @@ module Spree
219
219
  def gateway_order_id
220
220
  "#{order.number}-#{self.identifier}"
221
221
  end
222
+
223
+ def token_based?
224
+ source.gateway_customer_profile_id.present? || source.gateway_payment_profile_id.present?
225
+ end
222
226
  end
223
227
  end
224
228
  end
@@ -5,6 +5,7 @@ module Spree
5
5
 
6
6
  validate :check_price
7
7
  validates :amount, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
8
+ validate :validate_amount_maximum
8
9
 
9
10
  def display_amount
10
11
  money
@@ -50,5 +51,14 @@ module Spree
50
51
  price.to_d
51
52
  end
52
53
 
54
+ def maximum_amount
55
+ BigDecimal '999999.99'
56
+ end
57
+
58
+ def validate_amount_maximum
59
+ if amount && amount > maximum_amount
60
+ errors.add :amount, I18n.t('errors.messages.less_than_or_equal_to', count: maximum_amount)
61
+ end
62
+ end
53
63
  end
54
64
  end
@@ -57,6 +57,9 @@ module Spree
57
57
  has_many :prices, -> { order('spree_variants.position, spree_variants.id, currency') }, through: :variants
58
58
 
59
59
  has_many :stock_items, through: :variants_including_master
60
+
61
+ has_many :line_items, through: :variants_including_master
62
+ has_many :orders, through: :line_items
60
63
 
61
64
  delegate_belongs_to :master, :sku, :price, :currency, :display_amount, :display_price, :weight, :height, :width, :depth, :is_master, :has_default_price?, :cost_currency, :price_in, :amount_in
62
65
 
@@ -67,7 +70,8 @@ module Spree
67
70
  after_create :build_variants_from_option_values_hash, if: :option_values_hash
68
71
 
69
72
  after_save :save_master
70
- after_save :touch
73
+ after_save :run_touch_callbacks, if: :anything_changed?
74
+ after_save :reset_nested_changes
71
75
  after_touch :touch_taxons
72
76
 
73
77
  delegate :images, to: :master, prefix: true
@@ -199,10 +203,10 @@ module Spree
199
203
  end
200
204
 
201
205
  def total_on_hand
202
- if self.variants_including_master.any? { |v| !v.should_track_inventory? }
206
+ if any_variants_not_track_inventory?
203
207
  Float::INFINITY
204
208
  else
205
- self.stock_items.to_a.sum(&:count_on_hand)
209
+ stock_items.sum(:count_on_hand)
206
210
  end
207
211
  end
208
212
 
@@ -215,70 +219,93 @@ module Spree
215
219
 
216
220
  private
217
221
 
218
- def normalize_slug
219
- self.slug = normalize_friendly_id(slug)
220
- end
221
-
222
- # Builds variants from a hash of option types & values
223
- def build_variants_from_option_values_hash
224
- ensure_option_types_exist_for_values_hash
225
- values = option_values_hash.values
226
- values = values.inject(values.shift) { |memo, value| memo.product(value).map(&:flatten) }
227
-
228
- values.each do |ids|
229
- variant = variants.create(
230
- option_value_ids: ids,
231
- price: master.price
232
- )
222
+ def add_properties_and_option_types_from_prototype
223
+ if prototype_id && prototype = Spree::Prototype.find_by(id: prototype_id)
224
+ prototype.properties.each do |property|
225
+ product_properties.create(property: property)
233
226
  end
234
- save
227
+ self.option_types = prototype.option_types
235
228
  end
229
+ end
236
230
 
237
- def add_properties_and_option_types_from_prototype
238
- if prototype_id && prototype = Spree::Prototype.find_by(id: prototype_id)
239
- prototype.properties.each do |property|
240
- product_properties.create(property: property)
241
- end
242
- self.option_types = prototype.option_types
243
- end
231
+ def any_variants_not_track_inventory?
232
+ if variants_including_master.loaded?
233
+ variants_including_master.any? { |v| !v.should_track_inventory? }
234
+ else
235
+ !Spree::Config.track_inventory_levels || variants_including_master.where(track_inventory: false).any?
244
236
  end
237
+ end
245
238
 
246
- # ensures the master variant is flagged as such
247
- def set_master_variant_defaults
248
- master.is_master = true
239
+ # Builds variants from a hash of option types & values
240
+ def build_variants_from_option_values_hash
241
+ ensure_option_types_exist_for_values_hash
242
+ values = option_values_hash.values
243
+ values = values.inject(values.shift) { |memo, value| memo.product(value).map(&:flatten) }
244
+
245
+ values.each do |ids|
246
+ variant = variants.create(
247
+ option_value_ids: ids,
248
+ price: master.price
249
+ )
249
250
  end
251
+ save
252
+ end
250
253
 
251
- # there's a weird quirk with the delegate stuff that does not automatically save the delegate object
252
- # when saving so we force a save using a hook.
253
- def save_master
254
- master.save if master && (master.changed? || master.new_record? || (master.default_price && (master.default_price.changed? || master.default_price.new_record?)))
255
- end
254
+ def ensure_master
255
+ return unless new_record?
256
+ self.master ||= Variant.new
257
+ end
256
258
 
257
- def ensure_master
258
- return unless new_record?
259
- self.master ||= Variant.new
260
- end
259
+ def normalize_slug
260
+ self.slug = normalize_friendly_id(slug)
261
+ end
261
262
 
262
- # Iterate through this products taxons and taxonomies and touch their timestamps in a batch
263
- def touch_taxons
264
- taxons_to_touch = taxons.map(&:self_and_ancestors).flatten.uniq
265
- Spree::Taxon.where(id: taxons_to_touch.map(&:id)).update_all(updated_at: Time.current)
263
+ def punch_slug
264
+ update_column :slug, "#{Time.now.to_i}_#{slug}" # punch slug with date prefix to allow reuse of original
265
+ end
266
266
 
267
- taxonomy_ids_to_touch = taxons_to_touch.map(&:taxonomy_id).flatten.uniq
268
- Spree::Taxonomy.where(id: taxonomy_ids_to_touch).update_all(updated_at: Time.current)
269
- end
267
+ def anything_changed?
268
+ changed? || @nested_changes
269
+ end
270
270
 
271
- # Try building a slug based on the following fields in increasing order of specificity.
272
- def slug_candidates
273
- [
274
- :name,
275
- [:name, :sku]
276
- ]
277
- end
271
+ def reset_nested_changes
272
+ @nested_changes = false
273
+ end
278
274
 
279
- def punch_slug
280
- update(slug: "#{Time.now.to_i}_#{slug}") # punch slug with date prefix to allow reuse of original
275
+ # there's a weird quirk with the delegate stuff that does not automatically save the delegate object
276
+ # when saving so we force a save using a hook.
277
+ def save_master
278
+ if master && (master.changed? || master.new_record? || (master.default_price && (master.default_price.changed? || master.default_price.new_record?)))
279
+ master.save
280
+ @nested_changes = true
281
281
  end
282
+ end
283
+
284
+ # ensures the master variant is flagged as such
285
+ def set_master_variant_defaults
286
+ master.is_master = true
287
+ end
288
+
289
+ # Try building a slug based on the following fields in increasing order of specificity.
290
+ def slug_candidates
291
+ [
292
+ :name,
293
+ [:name, :sku]
294
+ ]
295
+ end
296
+
297
+ def run_touch_callbacks
298
+ run_callbacks(:touch)
299
+ end
300
+
301
+ # Iterate through this products taxons and taxonomies and touch their timestamps in a batch
302
+ def touch_taxons
303
+ taxons_to_touch = taxons.map(&:self_and_ancestors).flatten.uniq
304
+ Spree::Taxon.where(id: taxons_to_touch.map(&:id)).update_all(updated_at: Time.current)
305
+
306
+ taxonomy_ids_to_touch = taxons_to_touch.map(&:taxonomy_id).flatten.uniq
307
+ Spree::Taxonomy.where(id: taxonomy_ids_to_touch).update_all(updated_at: Time.current)
308
+ end
282
309
 
283
310
  end
284
311
  end
@@ -3,13 +3,14 @@ module Spree
3
3
  module Actions
4
4
  class CreateAdjustment < PromotionAction
5
5
  include Spree::Core::CalculatedAdjustments
6
+ include Spree::Core::AdjustmentSource
6
7
 
7
8
  has_many :adjustments, as: :source
8
9
 
9
10
  delegate :eligible?, to: :promotion
10
11
 
11
12
  before_validation :ensure_action_has_calculator
12
- before_destroy :deals_with_adjustments
13
+ before_destroy :deals_with_adjustments_for_deleted_source
13
14
 
14
15
  # Creates the adjustment related to a promotion for the order passed
15
16
  # through options hash
@@ -55,16 +56,6 @@ module Spree
55
56
  self.calculator = Calculator::FlatPercentItemTotal.new
56
57
  end
57
58
 
58
- def deals_with_adjustments
59
- adjustment_scope = self.adjustments.joins("LEFT OUTER JOIN spree_orders ON spree_orders.id = spree_adjustments.adjustable_id")
60
- # For incomplete orders, remove the adjustment completely.
61
- adjustment_scope.where("spree_orders.completed_at IS NULL").readonly(false).destroy_all
62
-
63
- # For complete orders, the source will be invalid.
64
- # Therefore we nullify the source_id, leaving the adjustment in place.
65
- # This would mean that the order's total is not altered at all.
66
- adjustment_scope.where("spree_orders.completed_at IS NOT NULL").update_all("source_id = NULL")
67
- end
68
59
  end
69
60
  end
70
61
  end
@@ -3,13 +3,14 @@ module Spree
3
3
  module Actions
4
4
  class CreateItemAdjustments < PromotionAction
5
5
  include Spree::Core::CalculatedAdjustments
6
+ include Spree::Core::AdjustmentSource
6
7
 
7
8
  has_many :adjustments, as: :source
8
9
 
9
10
  delegate :eligible?, to: :promotion
10
11
 
11
12
  before_validation :ensure_action_has_calculator
12
- before_destroy :deals_with_adjustments
13
+ before_destroy :deals_with_adjustments_for_deleted_source
13
14
 
14
15
  def perform(payload = {})
15
16
  order = payload[:order]
@@ -62,24 +63,6 @@ module Spree
62
63
  self.calculator = Calculator::PercentOnLineItem.new
63
64
  end
64
65
 
65
- def deals_with_adjustments
66
- adjustment_scope = self.adjustments.includes(:order).references(:spree_orders)
67
-
68
- # For incomplete orders, remove the adjustment completely.
69
- adjustment_scope.where("spree_orders.completed_at IS NULL").each do |adjustment|
70
- adjustment.destroy
71
- end
72
-
73
- # For complete orders, the source will be invalid.
74
- # Therefore we nullify the source_id, leaving the adjustment in place.
75
- # This would mean that the order's total is not altered at all.
76
- adjustment_scope.where("spree_orders.completed_at IS NOT NULL").each do |adjustment|
77
- adjustment.update_columns(
78
- source_id: nil,
79
- updated_at: Time.now,
80
- )
81
- end
82
- end
83
66
  end
84
67
  end
85
68
  end
@@ -29,9 +29,21 @@ module Spree
29
29
  end
30
30
 
31
31
  private
32
-
33
32
  def promotions
34
- Promotion.active.includes(:promotion_rules).where(:code => nil, :path => nil)
33
+ promo_table = Promotion.arel_table
34
+ join_table = Arel::Table.new(:spree_orders_promotions)
35
+
36
+ join_condition = promo_table.join(join_table, Arel::Nodes::OuterJoin).on(
37
+ promo_table[:id].eq(join_table[:promotion_id])
38
+ ).join_sources
39
+
40
+ Promotion.active.includes(:promotion_rules).
41
+ joins(join_condition).
42
+ where(
43
+ promo_table[:code].eq(nil).and(
44
+ promo_table[:path].eq(nil)
45
+ ).or(join_table[:order_id].eq(order.id))
46
+ ).distinct
35
47
  end
36
48
  end
37
49
  end