spree_core 3.0.1 → 3.0.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/app/models/friendly_id/slug_decorator.rb +3 -0
  3. data/app/models/spree/adjustment.rb +1 -1
  4. data/app/models/spree/calculator.rb +5 -0
  5. data/app/models/spree/customer_return.rb +22 -16
  6. data/app/models/spree/gateway/bogus.rb +4 -0
  7. data/app/models/spree/line_item.rb +1 -1
  8. data/app/models/spree/order.rb +22 -48
  9. data/app/models/spree/order/checkout.rb +270 -255
  10. data/app/models/spree/order_contents.rb +64 -61
  11. data/app/models/spree/order_merger.rb +65 -0
  12. data/app/models/spree/payment.rb +5 -0
  13. data/app/models/spree/payment/processing.rb +2 -1
  14. data/app/models/spree/payment_method/check.rb +12 -2
  15. data/app/models/spree/product.rb +5 -0
  16. data/app/models/spree/promotion_handler/cart.rb +18 -14
  17. data/app/models/spree/shipment.rb +1 -1
  18. data/app/models/spree/stock/availability_validator.rb +10 -9
  19. data/app/models/spree/stock/content_item.rb +8 -0
  20. data/app/models/spree/stock/package.rb +8 -0
  21. data/app/models/spree/stock_item.rb +5 -1
  22. data/app/models/spree/stock_movement.rb +6 -1
  23. data/app/models/spree/variant.rb +18 -15
  24. data/app/models/spree/zone.rb +39 -29
  25. data/app/views/spree/order_mailer/cancel_email.html.erb +1 -1
  26. data/app/views/spree/order_mailer/cancel_email.text.erb +1 -1
  27. data/app/views/spree/order_mailer/confirm_email.html.erb +1 -1
  28. data/app/views/spree/order_mailer/confirm_email.text.erb +1 -1
  29. data/config/initializers/user_class_extensions.rb +4 -0
  30. data/config/locales/en.yml +4 -3
  31. data/db/default/spree/default_reimbursement_type.rb +1 -0
  32. data/db/migrate/20150515211137_fix_adjustment_order_id.rb +70 -0
  33. data/db/migrate/20150522181728_add_deleted_at_to_friendly_id_slugs.rb +6 -0
  34. data/db/migrate/20150609093816_increase_scale_on_pre_tax_amounts.rb +16 -0
  35. data/db/migrate/20150707204155_enable_acts_as_paranoid_on_calculators.rb +6 -0
  36. data/lib/spree/core/controller_helpers/auth.rb +1 -1
  37. data/lib/spree/core/engine.rb +7 -0
  38. data/lib/spree/core/validators/email.rb +7 -3
  39. data/lib/spree/core/version.rb +1 -1
  40. data/lib/spree/permitted_attributes.rb +2 -2
  41. data/lib/spree/testing_support/common_rake.rb +3 -8
  42. data/lib/spree/testing_support/factories.rb +1 -1
  43. data/lib/spree/testing_support/factories/order_factory.rb +11 -0
  44. data/lib/spree/testing_support/order_walkthrough.rb +1 -1
  45. metadata +11 -4
@@ -7,7 +7,9 @@ module Spree
7
7
  end
8
8
 
9
9
  def add(variant, quantity = 1, options = {})
10
+ timestamp = Time.now
10
11
  line_item = add_to_line_item(variant, quantity, options)
12
+ options[:line_item_created] = true if timestamp <= line_item.created_at
11
13
  after_add_or_remove(line_item, options)
12
14
  end
13
15
 
@@ -22,10 +24,10 @@ module Spree
22
24
  # Update totals, then check if the order is eligible for any cart promotions.
23
25
  # If we do not update first, then the item total will be wrong and ItemTotal
24
26
  # promotion rules would not be triggered.
25
- reload_totals
27
+ persist_totals
26
28
  PromotionHandler::Cart.new(order).activate
27
29
  order.ensure_updated_shipments
28
- reload_totals
30
+ persist_totals
29
31
  true
30
32
  else
31
33
  false
@@ -33,80 +35,81 @@ module Spree
33
35
  end
34
36
 
35
37
  private
36
- def after_add_or_remove(line_item, options = {})
37
- reload_totals
38
- shipment = options[:shipment]
39
- shipment.present? ? shipment.update_amounts : order.ensure_updated_shipments
40
- PromotionHandler::Cart.new(order, line_item).activate
41
- Adjustable::AdjustmentsUpdater.update(line_item)
42
- reload_totals
43
- line_item
44
- end
45
38
 
46
- def filter_order_items(params)
47
- filtered_params = params.symbolize_keys
48
- return filtered_params if filtered_params[:line_items_attributes].nil? || filtered_params[:line_items_attributes][:id]
39
+ def after_add_or_remove(line_item, options = {})
40
+ persist_totals
41
+ shipment = options[:shipment]
42
+ shipment.present? ? shipment.update_amounts : order.ensure_updated_shipments
43
+ PromotionHandler::Cart.new(order, line_item).activate
44
+ Adjustable::AdjustmentsUpdater.update(line_item)
45
+ TaxRate.adjust(order, [line_item]) if options[:line_item_created]
46
+ persist_totals
47
+ line_item
48
+ end
49
+
50
+ def filter_order_items(params)
51
+ filtered_params = params.symbolize_keys
52
+ return filtered_params if filtered_params[:line_items_attributes].nil? || filtered_params[:line_items_attributes][:id]
49
53
 
50
- line_item_ids = order.line_items.pluck(:id)
54
+ line_item_ids = order.line_items.pluck(:id)
51
55
 
52
- params[:line_items_attributes].each_pair do |id, value|
53
- unless line_item_ids.include?(value[:id].to_i) || value[:variant_id].present?
54
- filtered_params[:line_items_attributes].delete(id)
55
- end
56
+ params[:line_items_attributes].each_pair do |id, value|
57
+ unless line_item_ids.include?(value[:id].to_i) || value[:variant_id].present?
58
+ filtered_params[:line_items_attributes].delete(id)
56
59
  end
57
- filtered_params
58
60
  end
61
+ filtered_params
62
+ end
59
63
 
60
- def order_updater
61
- @updater ||= OrderUpdater.new(order)
62
- end
64
+ def order_updater
65
+ @updater ||= OrderUpdater.new(order)
66
+ end
63
67
 
64
- def reload_totals
65
- order_updater.update_item_count
66
- order_updater.update
67
- order.reload
68
- end
68
+ def persist_totals
69
+ order_updater.update_item_count
70
+ order_updater.update
71
+ end
69
72
 
70
- def add_to_line_item(variant, quantity, options = {})
71
- line_item = grab_line_item_by_variant(variant, false, options)
72
-
73
- if line_item
74
- line_item.quantity += quantity.to_i
75
- line_item.currency = currency unless currency.nil?
76
- else
77
- opts = { currency: order.currency }.merge ActionController::Parameters.new(options).
78
- permit(PermittedAttributes.line_item_attributes)
79
- line_item = order.line_items.new(quantity: quantity,
80
- variant: variant,
81
- options: opts)
82
- end
83
- line_item.target_shipment = options[:shipment] if options.has_key? :shipment
84
- line_item.save!
85
- line_item
86
- end
73
+ def add_to_line_item(variant, quantity, options = {})
74
+ line_item = grab_line_item_by_variant(variant, false, options)
87
75
 
88
- def remove_from_line_item(variant, quantity, options = {})
89
- line_item = grab_line_item_by_variant(variant, true, options)
90
- line_item.quantity -= quantity
91
- line_item.target_shipment= options[:shipment]
76
+ if line_item
77
+ line_item.quantity += quantity.to_i
78
+ line_item.currency = currency unless currency.nil?
79
+ else
80
+ opts = { currency: order.currency }.merge ActionController::Parameters.new(options).
81
+ permit(PermittedAttributes.line_item_attributes)
82
+ line_item = order.line_items.new(quantity: quantity,
83
+ variant: variant,
84
+ options: opts)
85
+ end
86
+ line_item.target_shipment = options[:shipment] if options.has_key? :shipment
87
+ line_item.save!
88
+ line_item
89
+ end
92
90
 
93
- if line_item.quantity == 0
94
- line_item.destroy
95
- else
96
- line_item.save!
97
- end
91
+ def remove_from_line_item(variant, quantity, options = {})
92
+ line_item = grab_line_item_by_variant(variant, true, options)
93
+ line_item.quantity -= quantity
94
+ line_item.target_shipment= options[:shipment]
98
95
 
99
- line_item
96
+ if line_item.quantity.zero?
97
+ order.line_items.destroy(line_item)
98
+ else
99
+ line_item.save!
100
100
  end
101
101
 
102
- def grab_line_item_by_variant(variant, raise_error = false, options = {})
103
- line_item = order.find_line_item_by_variant(variant, options)
102
+ line_item
103
+ end
104
104
 
105
- if !line_item.present? && raise_error
106
- raise ActiveRecord::RecordNotFound, "Line item not found for variant #{variant.sku}"
107
- end
105
+ def grab_line_item_by_variant(variant, raise_error = false, options = {})
106
+ line_item = order.find_line_item_by_variant(variant, options)
108
107
 
109
- line_item
108
+ if !line_item.present? && raise_error
109
+ raise ActiveRecord::RecordNotFound, "Line item not found for variant #{variant.sku}"
110
110
  end
111
+
112
+ line_item
113
+ end
111
114
  end
112
115
  end
@@ -0,0 +1,65 @@
1
+ module Spree
2
+ class OrderMerger
3
+ attr_accessor :order
4
+ delegate :updater, to: :order
5
+
6
+ def initialize(order)
7
+ @order = order
8
+ end
9
+
10
+ def merge!(other_order, user = nil)
11
+ other_order.line_items.each do |other_order_line_item|
12
+ next unless other_order_line_item.currency == order.currency
13
+
14
+ current_line_item = find_matching_line_item(other_order_line_item)
15
+ handle_merge(current_line_item, other_order_line_item)
16
+ end
17
+
18
+ set_user(user)
19
+ persist_merge
20
+
21
+ # So that the destroy doesn't take out line items which may have been re-assigned
22
+ other_order.line_items.reload
23
+ other_order.destroy
24
+ end
25
+
26
+ # Compare the line item of the other order with mine.
27
+ # Make sure you allow any extensions to chime in on whether or
28
+ # not the extension-specific parts of the line item match
29
+ def find_matching_line_item(other_order_line_item)
30
+ order.line_items.detect do |my_li|
31
+ my_li.variant == other_order_line_item.variant &&
32
+ order.line_item_comparison_hooks.all? do |hook|
33
+ order.send(hook, my_li, other_order_line_item.serializable_hash)
34
+ end
35
+ end
36
+ end
37
+
38
+ def set_user(user = nil)
39
+ order.associate_user!(user) if !order.user && !user.blank?
40
+ end
41
+
42
+ # The idea is the end developer can choose to override the merge
43
+ # to their own choosing. Default is merge with errors.
44
+ def handle_merge(current_line_item, other_order_line_item)
45
+ if current_line_item
46
+ current_line_item.quantity += other_order_line_item.quantity
47
+ handle_error(current_line_item) unless current_line_item.save
48
+ else
49
+ other_order_line_item.order_id = order.id
50
+ handle_error(other_order_line_item) unless other_order_line_item.save
51
+ end
52
+ end
53
+
54
+ # Change the error messages as you choose.
55
+ def handle_error(line_item)
56
+ order.errors[:base] << line_item.errors.full_messages
57
+ end
58
+
59
+ def persist_merge
60
+ updater.update_item_count
61
+ updater.update_item_total
62
+ updater.persist_totals
63
+ end
64
+ end
65
+ end
@@ -26,6 +26,7 @@ module Spree
26
26
  has_many :capture_events, class_name: 'Spree::PaymentCaptureEvent'
27
27
  has_many :refunds, inverse_of: :payment
28
28
 
29
+ validates_presence_of :payment_method
29
30
  before_validation :validate_source
30
31
 
31
32
  after_save :create_payment_profile, if: :profiles_supported?
@@ -173,6 +174,10 @@ module Spree
173
174
  amount - captured_amount
174
175
  end
175
176
 
177
+ def editable?
178
+ checkout? || pending?
179
+ end
180
+
176
181
  private
177
182
 
178
183
  def validate_source
@@ -69,7 +69,8 @@ module Spree
69
69
  end
70
70
 
71
71
  def cancel!
72
- payment_method.cancel(response_code)
72
+ response = payment_method.cancel(response_code)
73
+ handle_response(response, :void, :failure)
73
74
  end
74
75
 
75
76
  def gateway_options
@@ -15,17 +15,27 @@ module Spree
15
15
  end
16
16
 
17
17
  def capture(*args)
18
- ActiveMerchant::Billing::Response.new(true, "", {}, {})
18
+ simulated_successful_billing_response
19
19
  end
20
20
 
21
21
  def cancel(response); end
22
22
 
23
23
  def void(*args)
24
- ActiveMerchant::Billing::Response.new(true, "", {}, {})
24
+ simulated_successful_billing_response
25
25
  end
26
26
 
27
27
  def source_required?
28
28
  false
29
29
  end
30
+
31
+ def credit(*args)
32
+ simulated_successful_billing_response
33
+ end
34
+
35
+ private
36
+
37
+ def simulated_successful_billing_response
38
+ ActiveMerchant::Billing::Response.new(true, "", {}, {})
39
+ end
30
40
  end
31
41
  end
@@ -74,6 +74,7 @@ module Spree
74
74
  after_create :build_variants_from_option_values_hash, if: :option_values_hash
75
75
 
76
76
  after_destroy :punch_slug
77
+ after_restore :update_slug_history
77
78
 
78
79
  after_initialize :ensure_master
79
80
 
@@ -267,6 +268,10 @@ module Spree
267
268
  update_column :slug, "#{Time.now.to_i}_#{slug}"[0..254] unless frozen?
268
269
  end
269
270
 
271
+ def update_slug_history
272
+ self.save!
273
+ end
274
+
270
275
  def anything_changed?
271
276
  changed? || @nested_changes
272
277
  end
@@ -29,22 +29,26 @@ module Spree
29
29
  end
30
30
 
31
31
  private
32
- def promotions
33
- promo_table = Promotion.arel_table
34
- join_table = Arel::Table.new(:spree_orders_promotions)
35
32
 
36
- join_condition = promo_table.join(join_table, Arel::Nodes::OuterJoin).on(
37
- promo_table[:id].eq(join_table[:promotion_id])
38
- ).join_sources
33
+ def promotions
34
+ # AR cannot bind raw ASTs to prepared statements. There always must be a manager around.
35
+ # Also Postgresql requires an aliased table for `SELECT * FROM (subexpression) AS alias`.
36
+ # And Sqlite3 cannot work on outher parenthesis from `(left UNION right)`.
37
+ # So this construct makes both happy.
38
+ select = Arel::SelectManager.new(
39
+ Promotion,
40
+ Promotion.arel_table.create_table_alias(
41
+ order.promotions.active.union(Promotion.active.where(code: nil, path: nil)),
42
+ Promotion.table_name
43
+ ),
44
+ )
45
+ select.project(Arel.star)
39
46
 
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
47
- end
47
+ Promotion.find_by_sql(
48
+ select,
49
+ order.promotions.bind_values
50
+ )
51
+ end
48
52
  end
49
53
  end
50
54
  end
@@ -157,7 +157,7 @@ module Spree
157
157
  end
158
158
 
159
159
  def item_cost
160
- line_items.map(&:final_amount).sum
160
+ manifest.map { |m| (m.line_item.price + (m.line_item.adjustment_total / m.line_item.quantity)) * m.quantity }.sum
161
161
  end
162
162
 
163
163
  def line_items
@@ -5,19 +5,20 @@ module Spree
5
5
  unit_count = line_item.inventory_units.size
6
6
  return if unit_count >= line_item.quantity
7
7
  quantity = line_item.quantity - unit_count
8
+ return if quantity.zero?
8
9
 
9
10
  quantifier = Stock::Quantifier.new(line_item.variant)
10
11
 
11
- unless quantifier.can_supply? quantity
12
- variant = line_item.variant
13
- display_name = %Q{#{variant.name}}
14
- display_name += %Q{ (#{variant.options_text})} unless variant.options_text.blank?
12
+ return if quantifier.can_supply?(quantity)
15
13
 
16
- line_item.errors[:quantity] << Spree.t(
17
- :selected_quantity_not_available,
18
- item: display_name.inspect
19
- )
20
- end
14
+ variant = line_item.variant
15
+ display_name = "#{variant.name}"
16
+ display_name += " (#{variant.options_text})" unless variant.options_text.blank?
17
+
18
+ line_item.errors[:quantity] << Spree.t(
19
+ :selected_quantity_not_available,
20
+ item: display_name.inspect
21
+ )
21
22
  end
22
23
  end
23
24
  end
@@ -43,6 +43,14 @@ module Spree
43
43
  # but this massively simplifies things for now
44
44
  1
45
45
  end
46
+
47
+ def volume
48
+ variant.volume * quantity
49
+ end
50
+
51
+ def dimension
52
+ variant.dimension * quantity
53
+ end
46
54
  end
47
55
  end
48
56
  end
@@ -91,6 +91,14 @@ module Spree
91
91
  def contents_by_weight
92
92
  contents.sort { |x, y| y.weight <=> x.weight }
93
93
  end
94
+
95
+ def volume
96
+ contents.sum(&:volume)
97
+ end
98
+
99
+ def dimension
100
+ contents.sum(&:dimension)
101
+ end
94
102
  end
95
103
  end
96
104
  end
@@ -8,7 +8,11 @@ module Spree
8
8
 
9
9
  validates_presence_of :stock_location, :variant
10
10
  validates_uniqueness_of :variant_id, scope: [:stock_location_id, :deleted_at]
11
- validates :count_on_hand, numericality: { greater_than_or_equal_to: 0 }, if: :verify_count_on_hand?
11
+
12
+ validates_numericality_of :count_on_hand,
13
+ greater_than_or_equal_to: 0,
14
+ less_than_or_equal_to: 2**31 - 1,
15
+ only_integer: true, if: :verify_count_on_hand?
12
16
 
13
17
  delegate :weight, :should_track_inventory?, to: :variant
14
18
 
@@ -6,7 +6,12 @@ module Spree
6
6
  after_create :update_stock_item_quantity
7
7
 
8
8
  validates :stock_item, presence: true
9
- validates :quantity, presence: true
9
+ validates :quantity, presence: true, numericality: {
10
+ greater_than_or_equal_to: -2**31,
11
+ less_than_or_equal_to: 2**31-1,
12
+ only_integer: true,
13
+ allow_nil: true
14
+ }
10
15
 
11
16
  scope :recent, -> { order('created_at DESC') }
12
17