spree_core 3.4.6 → 3.5.0.rc1

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 (70) hide show
  1. checksums.yaml +5 -5
  2. data/app/assets/javascripts/spree.js.coffee +1 -1
  3. data/app/helpers/spree/base_helper.rb +4 -0
  4. data/app/models/concerns/spree/named_type.rb +1 -1
  5. data/app/models/concerns/spree/user_methods.rb +21 -4
  6. data/app/models/concerns/spree/user_reporting.rb +2 -2
  7. data/app/models/spree/address.rb +6 -12
  8. data/app/models/spree/adjustable/adjustments_updater.rb +2 -1
  9. data/app/models/spree/country.rb +2 -1
  10. data/app/models/spree/line_item.rb +8 -2
  11. data/app/models/spree/log_entry.rb +1 -1
  12. data/app/models/spree/order.rb +8 -6
  13. data/app/models/spree/order/checkout.rb +1 -0
  14. data/app/models/spree/order_contents.rb +20 -12
  15. data/app/models/spree/order_inventory.rb +24 -12
  16. data/app/models/spree/payment/processing.rb +2 -2
  17. data/app/models/spree/preferences/preferable.rb +1 -1
  18. data/app/models/spree/product/scopes.rb +1 -1
  19. data/app/models/spree/promotion.rb +15 -1
  20. data/app/models/spree/promotion/rules/option_value.rb +13 -5
  21. data/app/models/spree/promotion/rules/product.rb +2 -1
  22. data/app/models/spree/promotion/rules/taxon.rb +3 -1
  23. data/app/models/spree/promotion_action_line_item.rb +3 -0
  24. data/app/models/spree/promotion_handler/promotion_duplicator.rb +52 -0
  25. data/app/models/spree/refund.rb +1 -1
  26. data/app/models/spree/reimbursement.rb +1 -1
  27. data/app/models/spree/reimbursement/reimbursement_type_engine.rb +7 -18
  28. data/app/models/spree/reimbursement_performer.rb +3 -7
  29. data/app/models/spree/reimbursement_type/original_payment.rb +2 -2
  30. data/app/models/spree/reimbursement_type/reimbursement_helpers.rb +3 -7
  31. data/app/models/spree/reimbursement_type/store_credit.rb +2 -10
  32. data/app/models/spree/shipment.rb +10 -4
  33. data/app/models/spree/stock/availability_validator.rb +1 -1
  34. data/app/models/spree/stock/packer.rb +1 -1
  35. data/app/models/spree/stock/splitter/backordered.rb +5 -7
  36. data/app/models/spree/stock/splitter/base.rb +1 -0
  37. data/app/models/spree/stock/splitter/shipping_category.rb +9 -16
  38. data/app/models/spree/stock/splitter/weight.rb +18 -20
  39. data/app/models/spree/stock_transfer.rb +2 -1
  40. data/app/models/spree/store_credit_category.rb +13 -0
  41. data/app/models/spree/taxon.rb +7 -0
  42. data/app/models/spree/variant.rb +1 -1
  43. data/app/validators/email_validator.rb +7 -0
  44. data/config/locales/en.yml +18 -27
  45. data/db/default/spree/states.rb +9 -27
  46. data/db/migrate/20150128032538_remove_environment_from_tracker.rb +2 -0
  47. data/db/migrate/20171004223836_remove_icon_from_taxons.rb +8 -0
  48. data/db/migrate/20180222133746_add_unique_index_on_spree_promotions_code.rb +6 -0
  49. data/lib/generators/spree/dummy_model/dummy_model_generator.rb +23 -0
  50. data/lib/generators/spree/dummy_model/templates/migration.rb.tt +10 -0
  51. data/lib/generators/spree/dummy_model/templates/model.rb.tt +6 -0
  52. data/lib/spree/core/controller_helpers/auth.rb +1 -1
  53. data/lib/spree/core/controller_helpers/common.rb +4 -0
  54. data/lib/spree/core/controller_helpers/order.rb +6 -5
  55. data/lib/spree/core/engine.rb +10 -10
  56. data/lib/spree/core/environment_extension.rb +3 -0
  57. data/lib/spree/core/importer/order.rb +1 -1
  58. data/lib/spree/core/validators/email.rb +1 -0
  59. data/lib/spree/core/version.rb +1 -1
  60. data/lib/spree/money.rb +1 -5
  61. data/lib/spree/permitted_attributes.rb +1 -1
  62. data/lib/spree/testing_support/capybara_ext.rb +16 -13
  63. data/lib/spree/testing_support/common_rake.rb +4 -1
  64. data/lib/spree/testing_support/factories/inventory_unit_factory.rb +7 -0
  65. data/lib/spree/testing_support/factories/taxon_factory.rb +1 -1
  66. data/spree_core.gemspec +1 -1
  67. data/vendor/assets/javascripts/jsuri.js +458 -2
  68. metadata +13 -7
  69. data/app/models/spree/tracker.rb +0 -25
  70. data/lib/spree/testing_support/factories/tracker_factory.rb +0 -7
@@ -31,12 +31,20 @@ module Spree
31
31
  end
32
32
 
33
33
  def actionable?(line_item)
34
- product_id = line_item.product.id
35
- option_values_ids = line_item.variant.option_value_ids
36
- eligible_product_ids = preferred_eligible_values.keys
37
- eligible_value_ids = preferred_eligible_values[product_id]
34
+ pid = line_item.product.id
35
+ ovids = line_item.variant.option_values.pluck(:id)
38
36
 
39
- eligible_product_ids.include?(product_id) && (eligible_value_ids & option_values_ids).present?
37
+ product_ids.include?(pid) && (value_ids(pid) - ovids).empty?
38
+ end
39
+
40
+ private
41
+
42
+ def product_ids
43
+ preferred_eligible_values.keys
44
+ end
45
+
46
+ def value_ids(product_id)
47
+ preferred_eligible_values[product_id]
40
48
  end
41
49
  end
42
50
  end
@@ -6,7 +6,8 @@ module Spree
6
6
  module Rules
7
7
  class Product < PromotionRule
8
8
  has_many :product_promotion_rules, class_name: 'Spree::ProductPromotionRule',
9
- foreign_key: :promotion_rule_id
9
+ foreign_key: :promotion_rule_id,
10
+ dependent: :destroy
10
11
  has_many :products, through: :product_promotion_rules, class_name: 'Spree::Product'
11
12
 
12
13
  MATCH_POLICIES = %w(any all none)
@@ -2,7 +2,9 @@ module Spree
2
2
  class Promotion
3
3
  module Rules
4
4
  class Taxon < PromotionRule
5
- has_many :promotion_rule_taxons, class_name: 'Spree::PromotionRuleTaxon', foreign_key: 'promotion_rule_id'
5
+ has_many :promotion_rule_taxons, class_name: 'Spree::PromotionRuleTaxon',
6
+ foreign_key: 'promotion_rule_id',
7
+ dependent: :destroy
6
8
  has_many :taxons, through: :promotion_rule_taxons, class_name: 'Spree::Taxon'
7
9
 
8
10
  MATCH_POLICIES = %w(any all)
@@ -2,5 +2,8 @@ module Spree
2
2
  class PromotionActionLineItem < Spree::Base
3
3
  belongs_to :promotion_action, class_name: 'Spree::Promotion::Actions::CreateLineItems'
4
4
  belongs_to :variant, class_name: 'Spree::Variant'
5
+
6
+ validates :promotion_action, :variant, :quantity, presence: true
7
+ validates :quantity, numericality: { only_integer: true, message: Spree.t('validation.must_be_int') }
5
8
  end
6
9
  end
@@ -0,0 +1,52 @@
1
+ module Spree
2
+ module PromotionHandler
3
+ class PromotionDuplicator
4
+ def initialize(promotion)
5
+ @promotion = promotion
6
+ end
7
+
8
+ def duplicate
9
+ @new_promotion = @promotion.dup
10
+ @new_promotion.path = "#{@promotion.path}_new"
11
+ @new_promotion.name = "New #{@promotion.name}"
12
+ @new_promotion.code = "#{@promotion.code}_new"
13
+
14
+ ActiveRecord::Base.transaction do
15
+ @new_promotion.save
16
+ copy_rules
17
+ copy_actions
18
+ end
19
+
20
+ @new_promotion
21
+ end
22
+
23
+ private
24
+
25
+ def copy_rules
26
+ @promotion.promotion_rules.each do |rule|
27
+ new_rule = rule.dup
28
+ @new_promotion.promotion_rules << new_rule
29
+
30
+ new_rule.users = rule.users if rule.try(:users)
31
+ new_rule.taxons = rule.taxons if rule.try(:taxons)
32
+ new_rule.products = rule.products if rule.try(:products)
33
+ end
34
+ end
35
+
36
+ def copy_actions
37
+ @promotion.promotion_actions.each do |action|
38
+ new_action = action.dup
39
+ new_action.calculator = action.calculator.dup if action.try(:calculator)
40
+
41
+ @new_promotion.promotion_actions << new_action
42
+
43
+ if action.try(:promotion_action_line_items)
44
+ action.promotion_action_line_items.each do |item|
45
+ new_action.promotion_action_line_items << item.dup
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -43,7 +43,7 @@ module Spree
43
43
  def perform!
44
44
  return true if transaction_id.present?
45
45
 
46
- credit_cents = Spree::Money.new(amount.to_f, currency: payment.currency).amount_in_cents
46
+ credit_cents = Spree::Money.new(amount.to_f, currency: payment.currency).money.cents
47
47
 
48
48
  @response = process!(credit_cents)
49
49
 
@@ -42,7 +42,7 @@ module Spree
42
42
  # Refund.total_amount_reimbursed_for(reimbursement)
43
43
  # See the `reimbursement_generator` property regarding the generation of custom reimbursements.
44
44
  class_attribute :reimbursement_models
45
- self.reimbursement_models = [Refund]
45
+ self.reimbursement_models = [Refund, Credit]
46
46
 
47
47
  # The reimbursement_performer property should be set to an object that responds to the following methods:
48
48
  # - #perform
@@ -22,7 +22,7 @@ module Spree
22
22
  def calculate_reimbursement_types
23
23
  @return_items.each do |return_item|
24
24
  reimbursement_type = calculate_reimbursement_type(return_item)
25
- add_reimbursement_type(return_item, reimbursement_type)
25
+ @reimbursement_type_hash[reimbursement_type] << return_item if reimbursement_type
26
26
  end
27
27
 
28
28
  @reimbursement_type_hash
@@ -31,24 +31,13 @@ module Spree
31
31
  private
32
32
 
33
33
  def calculate_reimbursement_type(return_item)
34
- if return_item.exchange_required?
35
- exchange_reimbursement_type
36
- elsif return_item.override_reimbursement_type.present?
37
- return_item.override_reimbursement_type.class
38
- elsif return_item.preferred_reimbursement_type.present?
39
- if valid_preferred_reimbursement_type?(return_item)
40
- return_item.preferred_reimbursement_type.class
41
- end
42
- elsif past_reimbursable_time_period?(return_item)
43
- expired_reimbursement_type
44
- else
45
- default_reimbursement_type
34
+ return exchange_reimbursement_type if return_item.exchange_required?
35
+ return return_item.override_reimbursement_type.class if return_item.override_reimbursement_type.present?
36
+ if return_item.preferred_reimbursement_type.present?
37
+ return valid_preferred_reimbursement_type?(return_item) ? return_item.preferred_reimbursement_type.class : nil
46
38
  end
47
- end
48
-
49
- def add_reimbursement_type(return_item, reimbursement_type)
50
- return unless reimbursement_type
51
- @reimbursement_type_hash[reimbursement_type] << return_item
39
+ return expired_reimbursement_type if past_reimbursable_time_period?(return_item)
40
+ default_reimbursement_type
52
41
  end
53
42
  end
54
43
  end
@@ -21,18 +21,14 @@ module Spree
21
21
  private
22
22
 
23
23
  def execute(reimbursement, simulate)
24
- reimbursement_type_hash = calculate_reimbursement_types(reimbursement)
24
+ # Engine reimbursement_type_engine returns hash of preferred reimbursement types pointing at return items
25
+ # {Spree::ReimbursementType::OriginalPayment => [ReturnItem, ...], Spree::ReimbursementType::Exchange => [ReturnItem, ...]}
26
+ reimbursement_type_hash = reimbursement_type_engine.new(reimbursement.return_items).calculate_reimbursement_types
25
27
 
26
28
  reimbursement_type_hash.flat_map do |reimbursement_type, return_items|
27
29
  reimbursement_type.reimburse(reimbursement, return_items, simulate)
28
30
  end
29
31
  end
30
-
31
- def calculate_reimbursement_types(reimbursement)
32
- # Engine returns hash of preferred reimbursement types pointing at return items
33
- # {Spree::ReimbursementType::OriginalPayment => [ReturnItem, ...], Spree::ReimbursementType::Exchange => [ReturnItem, ...]}
34
- reimbursement_type_engine.new(reimbursement.return_items).calculate_reimbursement_types
35
- end
36
32
  end
37
33
  end
38
34
  end
@@ -6,8 +6,8 @@ class Spree::ReimbursementType::OriginalPayment < Spree::ReimbursementType
6
6
  unpaid_amount = return_items.map { |ri| ri.total.to_d.round(2) }.sum
7
7
  payments = reimbursement.order.payments.completed
8
8
 
9
- refund_list, unpaid_amount = create_refunds(reimbursement, payments, unpaid_amount, simulate)
10
- refund_list
9
+ reimbursement_list, unpaid_amount = create_refunds(reimbursement, payments, unpaid_amount, simulate)
10
+ reimbursement_list
11
11
  end
12
12
  end
13
13
  end
@@ -1,7 +1,7 @@
1
1
  module Spree
2
2
  module ReimbursementType::ReimbursementHelpers
3
3
  def create_refunds(reimbursement, payments, unpaid_amount, simulate, reimbursement_list = [])
4
- payments.map do |payment|
4
+ payments.each do |payment|
5
5
  break if unpaid_amount <= 0
6
6
  next unless payment.can_credit?
7
7
 
@@ -37,17 +37,13 @@ module Spree
37
37
  # If you have multiple methods of crediting a customer, overwrite this method
38
38
  # Must return an array of objects the respond to #description, #display_amount
39
39
  def create_credit(reimbursement, unpaid_amount, simulate)
40
- creditable = create_creditable(reimbursement, unpaid_amount)
40
+ category = Spree::StoreCreditCategory.default_reimbursement_category(category_options(reimbursement))
41
+ creditable = Spree::StoreCredit.new(store_credit_params(category, reimbursement, unpaid_amount))
41
42
  credit = reimbursement.credits.build(creditable: creditable, amount: unpaid_amount)
42
43
  simulate ? credit.readonly! : credit.save!
43
44
  credit
44
45
  end
45
46
 
46
- def create_creditable(reimbursement, unpaid_amount)
47
- category = Spree::StoreCreditCategory.default_reimbursement_category(category_options(reimbursement))
48
- Spree::StoreCredit.new(store_credit_params(category, reimbursement, unpaid_amount))
49
- end
50
-
51
47
  def store_credit_params(category, reimbursement, unpaid_amount)
52
48
  {
53
49
  user: reimbursement.order.user,
@@ -4,12 +4,10 @@ class Spree::ReimbursementType::StoreCredit < Spree::ReimbursementType
4
4
  class << self
5
5
  def reimburse(reimbursement, return_items, simulate)
6
6
  unpaid_amount = return_items.sum(&:total).to_d.round(2, :down)
7
- payments = store_credit_payments(reimbursement)
8
- reimbursement_list = []
7
+ payments = reimbursement.order.payments.completed.store_credits
9
8
 
10
9
  # Credit each store credit that was used on the order
11
- reimbursement_list, unpaid_amount = create_refunds(reimbursement, payments, unpaid_amount,
12
- simulate, reimbursement_list)
10
+ reimbursement_list, unpaid_amount = create_refunds(reimbursement, payments, unpaid_amount, simulate)
13
11
 
14
12
  # If there is any amount left to pay out to the customer, then create credit with that amount
15
13
  if unpaid_amount > 0.0
@@ -18,11 +16,5 @@ class Spree::ReimbursementType::StoreCredit < Spree::ReimbursementType
18
16
 
19
17
  reimbursement_list
20
18
  end
21
-
22
- private
23
-
24
- def store_credit_payments(reimbursement)
25
- reimbursement.order.payments.completed.store_credits
26
- end
27
19
  end
28
20
  end
@@ -39,7 +39,7 @@ module Spree
39
39
  scope :trackable, -> { where("tracking IS NOT NULL AND tracking != ''") }
40
40
  scope :with_state, ->(*s) { where(state: s) }
41
41
  # sort by most recent shipped_at, falling back to created_at. add "id desc" to make specs that involve this scope more deterministic.
42
- scope :reverse_chronological, -> { order('coalesce(spree_shipments.shipped_at, spree_shipments.created_at) desc', id: :desc) }
42
+ scope :reverse_chronological, -> { order(Arel.sql('coalesce(spree_shipments.shipped_at, spree_shipments.created_at) desc'), id: :desc) }
43
43
 
44
44
  # shipment state machine (see http://github.com/pluginaweek/state_machine/tree/master for details)
45
45
  state_machine initial: :pending, use_transactions: false do
@@ -217,10 +217,15 @@ module Spree
217
217
 
218
218
  if shipping_method
219
219
  selected_rate = shipping_rates.detect do |rate|
220
- rate.shipping_method_id == original_shipping_method_id
220
+ if original_shipping_method_id
221
+ rate.shipping_method_id == original_shipping_method_id
222
+ else
223
+ rate.selected
224
+ end
221
225
  end
222
226
  save!
223
227
  self.selected_shipping_rate_id = selected_rate.id if selected_rate
228
+ reload
224
229
  end
225
230
 
226
231
  shipping_rates
@@ -340,10 +345,11 @@ module Spree
340
345
 
341
346
  order.contents.remove(variant, quantity, shipment: self)
342
347
  order.contents.add(variant, quantity, shipment: new_shipment)
348
+ order.create_tax_charge!
343
349
  order.update_with_updater!
344
350
 
345
351
  refresh_rates
346
- save!
352
+ save! if persisted?
347
353
  new_shipment.save!
348
354
  end
349
355
  end
@@ -362,7 +368,7 @@ module Spree
362
368
  order.update_with_updater!
363
369
 
364
370
  refresh_rates
365
- save!
371
+ save! if persisted?
366
372
  shipment_to_transfer_to.refresh_rates
367
373
  shipment_to_transfer_to.save!
368
374
  end
@@ -2,7 +2,7 @@ module Spree
2
2
  module Stock
3
3
  class AvailabilityValidator < ActiveModel::Validator
4
4
  def validate(line_item)
5
- unit_count = line_item.inventory_units.sum(&:quantity)
5
+ unit_count = line_item.inventory_units.reject(&:pending?).sum(&:quantity)
6
6
  return if unit_count >= line_item.quantity
7
7
 
8
8
  quantity = line_item.quantity - unit_count
@@ -41,7 +41,7 @@ module Spree
41
41
 
42
42
  def build_splitter
43
43
  splitter = nil
44
- splitters.reverse.each do |klass|
44
+ splitters.reverse_each do |klass|
45
45
  splitter = klass.new(self, splitter)
46
46
  end
47
47
  splitter
@@ -4,16 +4,14 @@ module Spree
4
4
  class Backordered < Spree::Stock::Splitter::Base
5
5
  def split(packages)
6
6
  split_packages = []
7
+
7
8
  packages.each do |package|
8
- unless package.on_hand.empty?
9
- split_packages << build_package(package.on_hand)
10
- end
9
+ split_packages << build_package(package.on_hand) unless package.on_hand.empty?
11
10
 
12
- unless package.backordered.empty?
13
- split_packages << build_package(package.backordered)
14
- end
11
+ split_packages << build_package(package.backordered) unless package.backordered.empty?
15
12
  end
16
- return_next split_packages
13
+
14
+ return_next(split_packages)
17
15
  end
18
16
  end
19
17
  end
@@ -8,6 +8,7 @@ module Spree
8
8
  @packer = packer
9
9
  @next_splitter = next_splitter
10
10
  end
11
+
11
12
  delegate :stock_location, to: :packer
12
13
 
13
14
  def split(packages)
@@ -3,31 +3,24 @@ module Spree
3
3
  module Splitter
4
4
  class ShippingCategory < Spree::Stock::Splitter::Base
5
5
  def split(packages)
6
- split_packages = []
7
- packages.each do |package|
8
- split_packages += split_by_category(package)
9
- end
10
- return_next split_packages
6
+ split_packages = packages.flat_map(&method(:split_by_category))
7
+ return_next(split_packages)
11
8
  end
12
9
 
13
10
  private
14
11
 
15
12
  def split_by_category(package)
16
- categories = Hash.new { |hash, key| hash[key] = [] }
17
- package.contents.each do |item|
18
- categories[shipping_category_for(item)] << item
19
- end
20
- hash_to_packages(categories)
13
+ # group package items by shipping category
14
+ grouped_packages = package.contents.group_by(&method(:shipping_category_for))
15
+ hash_to_packages(grouped_packages)
21
16
  end
22
17
 
23
- def hash_to_packages(categories)
24
- packages = []
25
- categories.each do |_id, contents|
26
- packages << build_package(contents)
27
- end
28
- packages
18
+ def hash_to_packages(grouped_packages)
19
+ # select values from packages grouped by shipping categories and build new packages
20
+ grouped_packages.values.map(&method(:build_package))
29
21
  end
30
22
 
23
+ # optimization: save variant -> shipping_category correspondence
31
24
  def shipping_category_for(item)
32
25
  @item_shipping_category ||= {}
33
26
  @item_shipping_category[item.inventory_unit.variant_id] ||= item.variant.shipping_category_id
@@ -4,17 +4,12 @@ module Spree
4
4
  class Weight < Spree::Stock::Splitter::Base
5
5
  attr_reader :packer, :next_splitter
6
6
 
7
- cattr_accessor :threshold do
8
- 150
9
- end
7
+ cattr_accessor(:threshold) { 150 }
10
8
 
11
9
  def split(packages)
12
- generated_packages = []
13
- packages.each do |package|
14
- generated_packages.push *reduce(package)
15
- end
16
- packages.push *generated_packages
17
- return_next packages
10
+ generated_packages = packages.flat_map(&method(:reduce))
11
+ packages.push(*generated_packages)
12
+ return_next(packages)
18
13
  end
19
14
 
20
15
  private
@@ -25,20 +20,20 @@ module Spree
25
20
  # This also prevents an additional package if no fit is possible
26
21
  package.contents.clear
27
22
  package.contents << contents.shift
28
- _split_packages = [package]
29
- while contents.present?
23
+ split_packages = [package]
30
24
 
31
- package_to_use = choose_package _split_packages, contents.first
25
+ while contents.present?
26
+ package_to_use = choose_package(split_packages, contents.first)
32
27
 
33
28
  if package_to_use.nil?
34
29
  package_to_use = build_package
35
- _split_packages << package_to_use
30
+ split_packages << package_to_use
36
31
  end
37
32
 
38
33
  package_to_use.contents << contents.shift
39
34
  end
40
35
 
41
- _split_packages.drop 1 # Drop the original package to ensure only generated packages are returned
36
+ split_packages.drop(1)
42
37
  end
43
38
 
44
39
  def choose_package(generated_packages, content_to_add)
@@ -50,11 +45,14 @@ module Spree
50
45
 
51
46
  generated_packages.each do |generated_package|
52
47
  generated_package_weight = generated_package.weight
53
- if (generated_package_weight + content_to_add.weight <= threshold) &&
54
- (available_space < threshold - generated_package_weight)
55
- package_to_use = generated_package
56
- available_space = threshold - generated_package_weight
57
- end
48
+
49
+ weight_exceed = (generated_package_weight + content_to_add.weight) > threshold
50
+ space_left = available_space >= (threshold - generated_package_weight)
51
+
52
+ next if weight_exceed || space_left
53
+
54
+ package_to_use = generated_package
55
+ available_space = threshold - generated_package_weight
58
56
  end
59
57
 
60
58
  package_to_use
@@ -63,7 +61,7 @@ module Spree
63
61
  def split_package_contents_over_threshold(package)
64
62
  package.contents.flat_map do |content|
65
63
  if content.weight > threshold && content.splittable_by_weight?
66
- split_content_item_over_threshold content
64
+ split_content_item_over_threshold(content)
67
65
  else
68
66
  content
69
67
  end