spree_core 4.8.3 → 4.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.md +57 -0
  3. data/app/finders/spree/line_items/find_by_variant.rb +4 -2
  4. data/app/finders/spree/orders/find_complete.rb +3 -13
  5. data/app/finders/spree/orders/find_current.rb +3 -1
  6. data/app/finders/spree/orders/finder_helper.rb +17 -0
  7. data/app/finders/spree/products/find.rb +1 -1
  8. data/app/finders/spree/variants/find.rb +30 -0
  9. data/app/models/spree/address.rb +12 -2
  10. data/app/models/spree/adjustment.rb +5 -0
  11. data/app/models/spree/asset.rb +8 -0
  12. data/app/models/spree/calculator/flat_rate.rb +3 -0
  13. data/app/models/spree/calculator/flexi_rate.rb +3 -0
  14. data/app/models/spree/calculator/percent_on_line_item.rb +3 -0
  15. data/app/models/spree/calculator/shipping/flat_rate.rb +13 -1
  16. data/app/models/spree/cms_section_image.rb +0 -6
  17. data/app/models/spree/customer_return.rb +1 -0
  18. data/app/models/spree/fulfilment_changer.rb +1 -0
  19. data/app/models/spree/icon.rb +0 -6
  20. data/app/models/spree/image/configuration/active_storage.rb +0 -8
  21. data/app/models/spree/image.rb +17 -0
  22. data/app/models/spree/line_item.rb +26 -7
  23. data/app/models/spree/option_value_variant.rb +4 -0
  24. data/app/models/spree/order/currency_updater.rb +2 -2
  25. data/app/models/spree/order/webhooks.rb +19 -0
  26. data/app/models/spree/order.rb +17 -6
  27. data/app/models/spree/order_merger.rb +6 -0
  28. data/app/models/spree/payment/gateway_options.rb +4 -0
  29. data/app/models/spree/payment/webhooks.rb +15 -0
  30. data/app/models/spree/payment.rb +7 -7
  31. data/app/models/spree/price.rb +53 -1
  32. data/app/models/spree/product/webhooks.rb +17 -0
  33. data/app/models/spree/product.rb +7 -9
  34. data/app/models/spree/promotion/actions/free_shipping.rb +13 -0
  35. data/app/models/spree/promotion/rules/first_order.rb +6 -1
  36. data/app/models/spree/promotion.rb +2 -2
  37. data/app/models/spree/promotion_handler/coupon.rb +3 -2
  38. data/app/models/spree/refund.rb +14 -2
  39. data/app/models/spree/reimbursement/emails.rb +11 -0
  40. data/app/models/spree/reimbursement.rb +1 -6
  41. data/app/models/spree/role_user.rb +1 -1
  42. data/app/models/spree/shipment/emails.rb +11 -0
  43. data/app/models/spree/shipment/webhooks.rb +11 -0
  44. data/app/models/spree/shipment.rb +60 -7
  45. data/app/models/spree/shipment_handler.rb +2 -6
  46. data/app/models/spree/shipping_method.rb +24 -2
  47. data/app/models/spree/shipping_method_zone.rb +4 -2
  48. data/app/models/spree/shipping_rate.rb +1 -1
  49. data/app/models/spree/stock/package.rb +4 -0
  50. data/app/models/spree/stock/quantifier.rb +22 -4
  51. data/app/models/spree/stock_item/webhooks.rb +6 -0
  52. data/app/models/spree/stock_item.rb +1 -3
  53. data/app/models/spree/stock_location.rb +15 -0
  54. data/app/models/spree/stock_movement/webhooks.rb +6 -0
  55. data/app/models/spree/stock_movement.rb +1 -3
  56. data/app/models/spree/store.rb +2 -9
  57. data/app/models/spree/store_credit.rb +11 -12
  58. data/app/models/spree/store_favicon_image.rb +0 -6
  59. data/app/models/spree/store_logo.rb +0 -5
  60. data/app/models/spree/store_mailer_logo.rb +0 -6
  61. data/app/models/spree/tax_rate.rb +3 -1
  62. data/app/models/spree/taxon.rb +83 -17
  63. data/app/models/spree/taxon_image/configuration/active_storage.rb +0 -6
  64. data/app/models/spree/taxonomy.rb +1 -0
  65. data/app/models/spree/variant/webhooks.rb +6 -0
  66. data/app/models/spree/variant.rb +29 -6
  67. data/app/models/spree/wishlist.rb +9 -0
  68. data/app/presenters/spree/variants/options_presenter.rb +34 -2
  69. data/app/services/spree/cart/add_item.rb +2 -0
  70. data/app/services/spree/cart/destroy.rb +14 -4
  71. data/app/services/spree/seeds/all.rb +3 -0
  72. data/app/services/spree/seeds/payment_methods.rb +18 -0
  73. data/app/services/spree/seeds/stock_locations.rb +1 -1
  74. data/app/services/spree/seeds/zones.rb +19 -19
  75. data/app/services/spree/tracking_numbers/base_service.rb +19 -0
  76. data/config/locales/en.yml +2 -0
  77. data/db/migrate/20240623172111_add_deleted_at_to_spree_stock_locations.rb +6 -0
  78. data/db/migrate/20240725124530_add_refunder_to_spree_refunds.rb +6 -0
  79. data/db/migrate/20240822163534_add_pretty_name_to_spree_taxons.rb +9 -0
  80. data/lib/spree/core/components.rb +6 -0
  81. data/lib/spree/core/controller_helpers/order.rb +5 -5
  82. data/lib/spree/core/dependencies.rb +5 -1
  83. data/lib/spree/core/engine.rb +1 -1
  84. data/lib/spree/core/preferences/preferable.rb +4 -2
  85. data/lib/spree/core/preferences/preferable_class_methods.rb +3 -2
  86. data/lib/spree/core/runtime_configuration.rb +1 -0
  87. data/lib/spree/core/version.rb +1 -1
  88. data/lib/spree/core/webhooks.rb +15 -7
  89. data/lib/spree/core.rb +1 -0
  90. data/lib/spree/money.rb +1 -1
  91. data/lib/spree/permitted_attributes.rb +1 -0
  92. data/lib/spree/testing_support/authorization_helpers.rb +2 -2
  93. data/lib/spree/testing_support/capybara_config.rb +8 -4
  94. data/lib/spree/testing_support/common_rake.rb +1 -1
  95. data/spree_core.gemspec +5 -3
  96. metadata +54 -8
  97. data/LICENSE +0 -26
@@ -0,0 +1,15 @@
1
+ module Spree
2
+ class Payment < Spree::Base
3
+ module Webhooks
4
+ extend ActiveSupport::Concern
5
+
6
+ def send_payment_voided_webhook
7
+ # Implement your logic here
8
+ end
9
+
10
+ def send_payment_completed_webhook
11
+ # Implement your logic here
12
+ end
13
+ end
14
+ end
15
+ end
@@ -6,14 +6,12 @@ module Spree
6
6
  include Spree::NumberIdentifier
7
7
  include Spree::NumberAsParam
8
8
  include Spree::Metadata
9
- if defined?(Spree::Webhooks::HasWebhooks)
10
- include Spree::Webhooks::HasWebhooks
11
- end
12
9
  if defined?(Spree::Security::Payments)
13
10
  include Spree::Security::Payments
14
11
  end
15
12
 
16
13
  include Spree::Payment::Processing
14
+ include Spree::Payment::Webhooks
17
15
 
18
16
  NON_RISKY_AVS_CODES = ['B', 'D', 'H', 'J', 'M', 'Q', 'T', 'V', 'X', 'Y'].freeze
19
17
  RISKY_AVS_CODES = ['A', 'C', 'E', 'F', 'G', 'I', 'K', 'L', 'N', 'O', 'P', 'R', 'S', 'U', 'W', 'Z'].freeze
@@ -67,6 +65,8 @@ module Spree
67
65
  scope :processing, -> { with_state('processing') }
68
66
  scope :failed, -> { with_state('failed') }
69
67
 
68
+ scope :incomplete, -> { where.not(state: 'completed') }
69
+
70
70
  scope :risky, -> { where("avs_response IN (?) OR (cvv_response_code IS NOT NULL and cvv_response_code != 'M') OR state = 'failed'", RISKY_AVS_CODES) }
71
71
  scope :valid, -> { where.not(state: INVALID_STATES) }
72
72
 
@@ -105,11 +105,11 @@ module Spree
105
105
  event :complete do
106
106
  transition from: [:processing, :pending, :checkout], to: :completed
107
107
  end
108
- after_transition to: :completed, do: :after_completed
108
+ after_transition to: :completed, do: [:after_completed, :send_payment_completed_webhook]
109
109
  event :void do
110
110
  transition from: [:pending, :processing, :completed, :checkout], to: :void
111
111
  end
112
- after_transition to: :void, do: :after_void
112
+ after_transition to: :void, do: [:after_void, :send_payment_voided_webhook]
113
113
  # when the card brand isn't supported
114
114
  event :invalidate do
115
115
  transition from: [:checkout], to: :invalid
@@ -209,11 +209,11 @@ module Spree
209
209
  private
210
210
 
211
211
  def after_void
212
- # this method is prepended in api/ to queue Webhooks requests
212
+ # Implement your logic here
213
213
  end
214
214
 
215
215
  def after_completed
216
- # this method is prepended in api/ to queue Webhooks requests
216
+ # Implement your logic here
217
217
  end
218
218
 
219
219
  def has_invalid_state?
@@ -12,17 +12,29 @@ module Spree
12
12
  belongs_to :variant, -> { with_deleted }, class_name: 'Spree::Variant', inverse_of: :prices, touch: true
13
13
 
14
14
  before_validation :ensure_currency
15
+ before_save :remove_compare_at_amount_if_equals_amount
15
16
 
17
+ # legacy behavior
16
18
  validates :amount, allow_nil: true, numericality: {
17
19
  greater_than_or_equal_to: 0,
18
20
  less_than_or_equal_to: MAXIMUM_AMOUNT
19
- }
21
+ }, if: -> { Spree::RuntimeConfig.allow_empty_price_amount }
22
+
23
+ # new behavior
24
+ validates :amount, allow_nil: false, numericality: {
25
+ greater_than_or_equal_to: 0,
26
+ less_than_or_equal_to: MAXIMUM_AMOUNT
27
+ }, unless: -> { Spree::RuntimeConfig.allow_empty_price_amount }
20
28
 
21
29
  validates :compare_at_amount, allow_nil: true, numericality: {
22
30
  greater_than_or_equal_to: 0,
23
31
  less_than_or_equal_to: MAXIMUM_AMOUNT
24
32
  }
25
33
 
34
+ scope :with_currency, ->(currency) { where(currency: currency) }
35
+ scope :non_zero, -> { where.not(amount: [nil, 0]) }
36
+ scope :discounted, -> { where('compare_at_amount > amount') }
37
+
26
38
  extend DisplayMoney
27
39
  money_methods :amount, :price, :compare_at_amount
28
40
  alias display_compare_at_price display_compare_at_amount
@@ -68,10 +80,50 @@ module Spree
68
80
  Spree::Money.new(compare_at_price_including_vat_for(price_options), currency: currency)
69
81
  end
70
82
 
83
+ # returns the name of the price in a format of variant name and currency
84
+ #
85
+ # @return [String]
86
+ def name
87
+ "#{variant.name} - #{currency.upcase}"
88
+ end
89
+
90
+ # returns true if the price is discounted
91
+ #
92
+ # @return [Boolean]
93
+ def discounted?
94
+ compare_at_amount.to_i.positive? && compare_at_amount > amount
95
+ end
96
+
97
+ # returns true if the price was discounted
98
+ #
99
+ # @return [Boolean]
100
+ def was_discounted?
101
+ compare_at_amount_was.to_i.positive? && compare_at_amount_was > amount_was
102
+ end
103
+
104
+ # returns true if the price is zero
105
+ #
106
+ # @return [Boolean]
107
+ def zero?
108
+ amount.nil? || amount.zero?
109
+ end
110
+
111
+ # returns true if the price is not zero
112
+ #
113
+ # @return [Boolean]
114
+ def non_zero?
115
+ !zero?
116
+ end
117
+
71
118
  private
72
119
 
73
120
  def ensure_currency
74
121
  self.currency ||= Spree::Store.default.default_currency
75
122
  end
123
+
124
+ # removes the compare at amount if it is the same as the amount
125
+ def remove_compare_at_amount_if_equals_amount
126
+ self.compare_at_amount = nil if compare_at_amount == amount
127
+ end
76
128
  end
77
129
  end
@@ -0,0 +1,17 @@
1
+ module Spree
2
+ class Product < Spree::Base
3
+ module Webhooks
4
+ def send_product_activated_webhook
5
+ # Implement your logic here
6
+ end
7
+
8
+ def send_product_archived_webhook
9
+ # Implement your logic here
10
+ end
11
+
12
+ def send_product_drafted_webhook
13
+ # Implement your logic here
14
+ end
15
+ end
16
+ end
17
+ end
@@ -27,9 +27,7 @@ module Spree
27
27
  include Spree::TranslatableResourceSlug
28
28
  include Spree::MemoizedData
29
29
  include Spree::Metadata
30
- if defined?(Spree::Webhooks::HasWebhooks)
31
- include Spree::Webhooks::HasWebhooks
32
- end
30
+ include Spree::Product::Webhooks
33
31
  if defined?(Spree::VendorConcern)
34
32
  include Spree::VendorConcern
35
33
  end
@@ -181,17 +179,17 @@ module Spree
181
179
  event :activate do
182
180
  transition to: :active
183
181
  end
184
- after_transition to: :active, do: :after_activate
182
+ after_transition to: :active, do: [:after_activate, :send_product_activated_webhook]
185
183
 
186
184
  event :archive do
187
185
  transition to: :archived
188
186
  end
189
- after_transition to: :archived, do: :after_archive
187
+ after_transition to: :archived, do: [:after_archive, :send_product_archived_webhook]
190
188
 
191
189
  event :draft do
192
190
  transition to: :draft
193
191
  end
194
- after_transition to: :draft, do: :after_draft
192
+ after_transition to: :draft, do: [:after_draft, :send_product_drafted_webhook]
195
193
  end
196
194
 
197
195
  # Can't use short form block syntax due to https://github.com/Netflix/fast_jsonapi/issues/259
@@ -584,15 +582,15 @@ module Spree
584
582
  end
585
583
 
586
584
  def after_activate
587
- # this method is prepended in api/ to queue Webhooks requests
585
+ # Implement your logic here
588
586
  end
589
587
 
590
588
  def after_archive
591
- # this method is prepended in api/ to queue Webhooks requests
589
+ # Implement your logic here
592
590
  end
593
591
 
594
592
  def after_draft
595
- # this method is prepended in api/ to queue Webhooks requests
593
+ # Implement your logic here
596
594
  end
597
595
  end
598
596
  end
@@ -13,6 +13,19 @@ module Spree
13
13
  def compute_amount(shipment)
14
14
  shipment.cost * -1
15
15
  end
16
+
17
+ # we need to persist 0 amount adjustment
18
+ def create_adjustment(order, adjustable, included = false)
19
+ amount = compute_amount(adjustable)
20
+
21
+ adjustments.new(
22
+ adjustable: adjustable,
23
+ amount: amount,
24
+ included: included,
25
+ label: label,
26
+ order: order
27
+ ).save
28
+ end
16
29
  end
17
30
  end
18
31
  end
@@ -10,7 +10,12 @@ module Spree
10
10
 
11
11
  def eligible?(order, options = {})
12
12
  @user = order.try(:user) || options[:user]
13
- @email = order.email
13
+ @email = if options[:email].present?
14
+ order.email = options[:email]
15
+ order.email
16
+ elsif order.email.present?
17
+ order.email
18
+ end
14
19
  @store = order.store
15
20
 
16
21
  if user || email
@@ -128,10 +128,10 @@ module Spree
128
128
  end
129
129
 
130
130
  # called anytime order.update_with_updater! happens
131
- def eligible?(promotable)
131
+ def eligible?(promotable, options = {})
132
132
  return false if expired? || usage_limit_exceeded?(promotable) || blacklisted?(promotable)
133
133
 
134
- !!eligible_rules(promotable, {})
134
+ !!eligible_rules(promotable, options)
135
135
  end
136
136
 
137
137
  # eligible_rules returns an array of promotion rules where eligible? is true for the promotable
@@ -1,12 +1,13 @@
1
1
  module Spree
2
2
  module PromotionHandler
3
3
  class Coupon
4
- attr_reader :order, :store
4
+ attr_reader :order, :store, :options
5
5
  attr_accessor :error, :success, :status_code
6
6
 
7
- def initialize(order)
7
+ def initialize(order, options = {})
8
8
  @order = order
9
9
  @store = order.store
10
+ @options = options
10
11
  end
11
12
 
12
13
  def apply
@@ -13,6 +13,7 @@ module Spree
13
13
  belongs_to :reimbursement, optional: true
14
14
  end
15
15
  belongs_to :reason, class_name: 'Spree::RefundReason', foreign_key: :refund_reason_id
16
+ belongs_to :refunder, class_name: "::#{Spree.admin_user_class}", optional: true
16
17
 
17
18
  has_many :log_entries, as: :source
18
19
 
@@ -31,8 +32,10 @@ module Spree
31
32
 
32
33
  attr_reader :response
33
34
 
35
+ delegate :order, :currency, to: :payment
36
+
34
37
  def money
35
- Spree::Money.new(amount, currency: payment.currency)
38
+ Spree::Money.new(amount, currency: currency)
36
39
  end
37
40
  alias display_amount money
38
41
 
@@ -46,6 +49,15 @@ module Spree
46
49
  payment.payment_method.name
47
50
  end
48
51
 
52
+ # return items for the refund
53
+ #
54
+ # @return [Array<Spree::ReturnItem>]
55
+ def return_items
56
+ return [] unless reimbursement.present?
57
+
58
+ reimbursement.customer_return&.return_items || reimbursement.return_items
59
+ end
60
+
49
61
  private
50
62
 
51
63
  # attempts to perform the refund.
@@ -53,7 +65,7 @@ module Spree
53
65
  def perform!
54
66
  return true if transaction_id.present?
55
67
 
56
- credit_cents = Spree::Money.new(amount.to_f, currency: payment.currency).amount_in_cents
68
+ credit_cents = Spree::Money.new(amount.to_f, currency: currency).amount_in_cents
57
69
 
58
70
  @response = process!(credit_cents)
59
71
 
@@ -0,0 +1,11 @@
1
+ module Spree
2
+ class Reimbursement < Spree::Base
3
+ module Emails
4
+ def send_reimbursement_email
5
+ # you can overwrite this method in your application / extension to send out the confirmation email
6
+ # or use `spree_emails` gem
7
+ # YourEmailVendor.deliver_reimbursement_email(id) # `id` = ID of the Reimbursement being sent, you can also pass the entire object using `self`
8
+ end
9
+ end
10
+ end
11
+ end
@@ -5,6 +5,7 @@ module Spree
5
5
  if defined?(Spree::Webhooks::HasWebhooks)
6
6
  include Spree::Webhooks::HasWebhooks
7
7
  end
8
+ include Spree::Reimbursement::Emails
8
9
 
9
10
  class IncompleteReimbursementError < StandardError; end
10
11
 
@@ -141,12 +142,6 @@ module Spree
141
142
  end
142
143
  end
143
144
 
144
- def send_reimbursement_email
145
- # you can overwrite this method in your application / extension to send out the confirmation email
146
- # or use `spree_emails` gem
147
- # YourEmailVendor.deliver_reimbursement_email(id) # `id` = ID of the Reimbursement being sent, you can also pass the entire object using `self`
148
- end
149
-
150
145
  # If there are multiple different reimbursement types for a single
151
146
  # reimbursement we open ourselves to a one-cent rounding error for every
152
147
  # type over the first one. This is due to how we round #unpaid_amount and
@@ -1,6 +1,6 @@
1
1
  module Spree
2
2
  class RoleUser < Spree::Base
3
3
  belongs_to :role, class_name: 'Spree::Role'
4
- belongs_to :user, class_name: "::#{Spree.user_class}"
4
+ belongs_to :user, class_name: Spree.admin_user_class.to_s
5
5
  end
6
6
  end
@@ -0,0 +1,11 @@
1
+ module Spree
2
+ class Shipment < Spree::Base
3
+ module Emails
4
+ def send_shipped_email
5
+ # you can overwrite this method in your application / extension to send out the confirmation email
6
+ # or use `spree_emails` gem
7
+ # YourEmailVendor.deliver_shipment_notification_email(@shipment.id)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Spree
2
+ class Shipment < Spree::Base
3
+ module Webhooks
4
+ extend ActiveSupport::Concern
5
+
6
+ def send_shipment_shipped_webhook
7
+ # Implement your logic here
8
+ end
9
+ end
10
+ end
11
+ end
@@ -6,18 +6,20 @@ module Spree
6
6
  include Spree::NumberIdentifier
7
7
  include Spree::NumberAsParam
8
8
  include Spree::Metadata
9
- if defined?(Spree::Webhooks::HasWebhooks)
10
- include Spree::Webhooks::HasWebhooks
11
- end
12
9
  if defined?(Spree::Security::Shipments)
13
10
  include Spree::Security::Shipments
14
11
  end
12
+ if defined?(Spree::VendorConcern)
13
+ include Spree::VendorConcern
14
+ end
15
+ include Spree::Shipment::Emails
16
+ include Spree::Shipment::Webhooks
15
17
 
16
18
  with_options inverse_of: :shipments do
17
19
  belongs_to :address, class_name: 'Spree::Address'
18
20
  belongs_to :order, class_name: 'Spree::Order', touch: true
19
21
  end
20
- belongs_to :stock_location, class_name: 'Spree::StockLocation'
22
+ belongs_to :stock_location, -> { with_deleted }, class_name: 'Spree::StockLocation'
21
23
 
22
24
  with_options dependent: :delete_all do
23
25
  has_many :adjustments, as: :adjustable
@@ -26,6 +28,7 @@ module Spree
26
28
  has_many :state_changes, as: :stateful
27
29
  end
28
30
  has_many :shipping_methods, through: :shipping_rates
31
+ has_many :variants, through: :inventory_units
29
32
  has_one :selected_shipping_rate, -> { where(selected: true).order(:cost) }, class_name: Spree::ShippingRate.to_s
30
33
 
31
34
  after_save :update_adjustments
@@ -47,8 +50,18 @@ module Spree
47
50
  # sort by most recent shipped_at, falling back to created_at. add "id desc" to make specs that involve this scope more deterministic.
48
51
  scope :reverse_chronological, -> { order(Arel.sql('coalesce(spree_shipments.shipped_at, spree_shipments.created_at) desc'), id: :desc) }
49
52
  scope :valid, -> { where.not(state: :canceled) }
53
+ scope :canceled, -> { with_state('canceled') }
54
+ scope :not_canceled, -> { where.not(state: 'canceled') }
55
+ scope :shipped_but_canceled, -> { canceled.where.not(shipped_at: nil) }
56
+ # This scope will select the shipping_method_id from the shipments' selected shipping rate
57
+ scope :with_selected_shipping_method, lambda {
58
+ joins(:selected_shipping_rate).
59
+ where(Spree::ShippingRate.arel_table[:shipping_method_id].not_eq(nil)).
60
+ select(Spree::ShippingRate.arel_table[:shipping_method_id])
61
+ }
50
62
 
51
63
  delegate :store, :currency, to: :order
64
+ delegate :amount_in_cents, to: :display_cost
52
65
 
53
66
  # shipment state machine (see http://github.com/pluginaweek/state_machine/tree/master for details)
54
67
  state_machine initial: :pending, use_transactions: false do
@@ -66,7 +79,7 @@ module Spree
66
79
  event :ship do
67
80
  transition from: %i(ready canceled), to: :shipped
68
81
  end
69
- after_transition to: :shipped, do: :after_ship
82
+ after_transition to: :shipped, do: [:after_ship, :send_shipment_shipped_webhook]
70
83
 
71
84
  event :cancel do
72
85
  transition to: :canceled, from: %i(pending ready)
@@ -93,9 +106,22 @@ module Spree
93
106
  self.whitelisted_ransackable_attributes = ['number']
94
107
 
95
108
  extend DisplayMoney
96
- money_methods :cost, :discounted_cost, :final_price, :item_cost
109
+ money_methods :cost, :discounted_cost, :final_price, :item_cost, :additional_tax_total, :included_tax_total, :tax_total
97
110
  alias display_amount display_cost
98
111
 
112
+ auto_strip_attributes :tracking
113
+
114
+ # Returns the shipment number and shipping method name
115
+ #
116
+ # @return [String]
117
+ def name
118
+ [number, shipping_method&.name].compact.join(' ').strip
119
+ end
120
+
121
+ def amount
122
+ cost
123
+ end
124
+
99
125
  def add_shipping_method(shipping_method, selected = false)
100
126
  shipping_rates.create(shipping_method: shipping_method, selected: selected, cost: cost)
101
127
  end
@@ -108,17 +134,44 @@ module Spree
108
134
  manifest.each { |item| manifest_unstock(item) }
109
135
  end
110
136
 
137
+ # Returns true if the shipment has any backordered inventory units
138
+ #
139
+ # @return [Boolean]
111
140
  def backordered?
112
141
  inventory_units.any?(&:backordered?)
113
142
  end
114
143
 
144
+ # Returns true if the shipment is tracked
145
+ #
146
+ # @return [Boolean]
147
+ def tracked?
148
+ tracking.present? || tracking_url.present?
149
+ end
150
+
151
+ # Returns true if the shipment is shippable
152
+ #
153
+ # @return [Boolean]
154
+ def shippable?
155
+ can_ship? && tracked?
156
+ end
157
+
158
+ # Returns true if not all of the shipment's line items are fully shipped
159
+ #
160
+ # @return [Boolean]
161
+ def partial?
162
+ manifest.any? do |manifest_item|
163
+ line_item = manifest_item.line_item
164
+ line_item.quantity > manifest_item.quantity
165
+ end
166
+ end
167
+
115
168
  # Determines the appropriate +state+ according to the following logic:
116
169
  #
117
170
  # pending unless order is complete and +order.payment_state+ is +paid+
118
171
  # shipped if already shipped (ie. does not change the state)
119
172
  # ready all other cases
120
173
  def determine_state(order)
121
- return 'canceled' if order.canceled?
174
+ return 'canceled' if canceled? || order.canceled?
122
175
  return 'pending' unless order.can_ship?
123
176
  return 'pending' if inventory_units.any? &:backordered?
124
177
  return 'shipped' if shipped?
@@ -1,5 +1,7 @@
1
1
  module Spree
2
2
  class ShipmentHandler
3
+ include Spree::Shipment::Emails
4
+
3
5
  class << self
4
6
  def factory(shipment)
5
7
  # Do we have a specialized shipping-method-specific handler? e.g:
@@ -27,12 +29,6 @@ module Spree
27
29
 
28
30
  protected
29
31
 
30
- def send_shipped_email
31
- # you can overwrite this method in your application / extension to send out the confirmation email
32
- # or use `spree_emails` gem
33
- # YourEmailVendor.deliver_shipment_notification_email(@shipment.id)
34
- end
35
-
36
32
  def update_order_shipment_state
37
33
  order = @shipment.order
38
34
 
@@ -18,6 +18,8 @@ module Spree
18
18
 
19
19
  default_scope { where(deleted_at: nil) }
20
20
 
21
+ scope :with_calculator, ->(calculator) { joins(:calculator).where(calculator: { type: calculator.to_s }) }
22
+
21
23
  has_many :shipping_method_categories, dependent: :destroy
22
24
  has_many :shipping_categories, through: :shipping_method_categories
23
25
  has_many :shipping_rates, inverse_of: :shipping_method
@@ -33,6 +35,10 @@ module Spree
33
35
 
34
36
  validate :at_least_one_shipping_category
35
37
 
38
+ scope :available, -> { where(display_on: [:both]) }
39
+ scope :available_on_front_end, -> { where(display_on: [:front_end, :both]) }
40
+ scope :available_on_back_end, -> { where(display_on: [:back_end, :both]) }
41
+
36
42
  def include?(address)
37
43
  return false unless address
38
44
 
@@ -42,9 +48,25 @@ module Spree
42
48
  end
43
49
 
44
50
  def build_tracking_url(tracking)
45
- return if tracking.blank? || tracking_url.blank?
51
+ return if tracking.blank?
52
+
53
+ tracking = tracking.upcase
54
+
55
+ # build tracking url automatically
56
+ if tracking_url.blank?
57
+ # use tracking number gem to build tracking url
58
+ # we need to upcase the tracking number
59
+ # https://github.com/jkeen/tracking_number/pull/85
60
+ tracking_number_service(tracking).tracking_url if tracking_number_service(tracking).valid?
61
+ else
62
+ # build tracking url manually
63
+ tracking_url.gsub(/:tracking/, ERB::Util.url_encode(tracking)) # :url_encode exists in 1.8.7 through 2.1.0
64
+ end
65
+ end
46
66
 
47
- tracking_url.gsub(/:tracking/, ERB::Util.url_encode(tracking)) # :url_encode exists in 1.8.7 through 2.1.0
67
+ # your shipping method subclasses can override this method to provide a custom tracking number service
68
+ def tracking_number_service(tracking)
69
+ @tracking_number_service ||= Spree::Dependencies.tracking_number_service.constantize.new(tracking)
48
70
  end
49
71
 
50
72
  def self.calculators
@@ -1,6 +1,8 @@
1
1
  module Spree
2
2
  class ShippingMethodZone < Spree::Base
3
- belongs_to :shipping_method
4
- belongs_to :zone
3
+ belongs_to :shipping_method, -> { with_deleted }, inverse_of: :shipping_method_zones, class_name: 'Spree::ShippingMethod'
4
+ belongs_to :zone, inverse_of: :shipping_method_zones, class_name: 'Spree::Zone'
5
+
6
+ validates :shipping_method, uniqueness: { scope: :zone }
5
7
  end
6
8
  end
@@ -14,7 +14,7 @@ module Spree
14
14
  def display_price
15
15
  price = display_base_price.to_s
16
16
 
17
- return price if tax_rate.nil? || tax_amount.zero?
17
+ return price if tax_rate.nil? || tax_amount.zero? || !tax_rate.show_rate_in_label
18
18
 
19
19
  Spree.t(
20
20
  tax_rate.included_in_price? ? :including_tax : :excluding_tax,
@@ -34,6 +34,10 @@ module Spree
34
34
  contents.detect { |item| !!item.try(:inventory_unit).try(:order) }.try(:inventory_unit).try(:order)
35
35
  end
36
36
 
37
+ def item_total
38
+ contents.sum(&:amount)
39
+ end
40
+
37
41
  def weight
38
42
  contents.sum(&:weight)
39
43
  end
@@ -10,7 +10,11 @@ module Spree
10
10
 
11
11
  def total_on_hand
12
12
  @total_on_hand ||= if variant.should_track_inventory?
13
- stock_items.sum(:count_on_hand)
13
+ if association_loaded?
14
+ stock_items.sum(&:count_on_hand)
15
+ else
16
+ stock_items.sum(:count_on_hand)
17
+ end
14
18
  else
15
19
  BigDecimal::INFINITY
16
20
  end
@@ -30,10 +34,24 @@ module Spree
30
34
 
31
35
  private
32
36
 
33
- def scope_to_location(collection)
34
- return collection.with_active_stock_location if stock_location.blank?
37
+ def association_loaded?
38
+ variant.association(:stock_items).loaded? && variant.association(:stock_locations).loaded?
39
+ end
35
40
 
36
- collection.where(stock_location: stock_location)
41
+ def scope_to_location(collection)
42
+ if stock_location.blank?
43
+ if association_loaded?
44
+ return collection.select { |si| si.stock_location&.active? }
45
+ else
46
+ return collection.with_active_stock_location
47
+ end
48
+ end
49
+
50
+ if association_loaded?
51
+ collection.select { |si| si.stock_location_id == stock_location.id }
52
+ else
53
+ collection.where(stock_location: stock_location)
54
+ end
37
55
  end
38
56
  end
39
57
  end