spree_core 2.3.1 → 2.3.2

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