spree_core 3.4.6 → 3.5.0.rc1

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