/
variant.rb
270 lines (212 loc) 路 7.86 KB
/
variant.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
module Spree
class Variant < Spree::Base
acts_as_paranoid
acts_as_list scope: :product
include Spree::DefaultPrice
belongs_to :product, touch: true, class_name: 'Spree::Product', inverse_of: :variants
belongs_to :tax_category, class_name: 'Spree::TaxCategory'
delegate_belongs_to :product, :name, :description, :slug, :available_on,
:shipping_category_id, :meta_description, :meta_keywords,
:shipping_category
has_many :inventory_units, inverse_of: :variant
has_many :line_items, inverse_of: :variant
has_many :orders, through: :line_items
has_many :stock_items, dependent: :destroy, inverse_of: :variant
has_many :stock_locations, through: :stock_items
has_many :stock_movements, through: :stock_items
has_and_belongs_to_many :option_values, join_table: :spree_option_values_variants
has_many :images, -> { order(:position) }, as: :viewable, dependent: :destroy, class_name: "Spree::Image"
has_many :prices,
class_name: 'Spree::Price',
dependent: :destroy,
inverse_of: :variant
before_validation :set_cost_currency
validate :check_price
validates :cost_price, numericality: { greater_than_or_equal_to: 0, allow_nil: true }
validates :price, numericality: { greater_than_or_equal_to: 0, allow_nil: true }
validates_uniqueness_of :sku, allow_blank: true, conditions: -> { where(deleted_at: nil) }
after_create :create_stock_items
after_create :set_master_out_of_stock, unless: :is_master?
after_touch :clear_in_stock_cache
scope :in_stock, -> { joins(:stock_items).where('count_on_hand > ? OR track_inventory = ?', 0, false) }
LOCALIZED_NUMBERS = %w(cost_price weight depth width height)
LOCALIZED_NUMBERS.each do |m|
define_method("#{m}=") do |argument|
self[m] = Spree::LocalizedNumber.parse(argument) if argument.present?
end
end
self.whitelisted_ransackable_associations = %w[option_values product prices default_price]
self.whitelisted_ransackable_attributes = %w[weight sku]
def self.active(currency = nil)
joins(:prices).where(deleted_at: nil).where('spree_prices.currency' => currency || Spree::Config[:currency]).where('spree_prices.amount IS NOT NULL')
end
def self.having_orders
joins(:line_items).distinct
end
def tax_category
if self[:tax_category_id].nil?
product.tax_category
else
TaxCategory.find(self[:tax_category_id])
end
end
# returns number of units currently on backorder for this variant.
def on_backorder
inventory_units.with_state('backordered').size
end
def is_backorderable?
Spree::Stock::Quantifier.new(self).backorderable?
end
def options_text
values = self.option_values.sort do |a, b|
a.option_type.position <=> b.option_type.position
end
values.to_a.map! do |ov|
"#{ov.option_type.presentation}: #{ov.presentation}"
end
values.to_sentence({ words_connector: ", ", two_words_connector: ", " })
end
# Default to master name
def exchange_name
is_master? ? name : options_text
end
def descriptive_name
is_master? ? name + ' - Master' : name + ' - ' + options_text
end
# use deleted? rather than checking the attribute directly. this
# allows extensions to override deleted? if they want to provide
# their own definition.
def deleted?
!!deleted_at
end
# Product may be created with deleted_at already set,
# which would make AR's default finder return nil.
# This is a stopgap for that little problem.
def product
Spree::Product.unscoped { super }
end
def options=(options = {})
options.each do |option|
set_option_value(option[:name], option[:value])
end
end
def set_option_value(opt_name, opt_value)
# no option values on master
return if self.is_master
option_type = Spree::OptionType.where(name: opt_name).first_or_initialize do |o|
o.presentation = opt_name
o.save!
end
current_value = self.option_values.detect { |o| o.option_type.name == opt_name }
unless current_value.nil?
return if current_value.name == opt_value
self.option_values.delete(current_value)
else
# then we have to check to make sure that the product has the option type
unless self.product.option_types.include? option_type
self.product.option_types << option_type
end
end
option_value = Spree::OptionValue.where(option_type_id: option_type.id, name: opt_value).first_or_initialize do |o|
o.presentation = opt_value
o.save!
end
self.option_values << option_value
self.save
end
def option_value(opt_name)
self.option_values.detect { |o| o.option_type.name == opt_name }.try(:presentation)
end
def price_in(currency)
prices.detect { |price| price.currency == currency } || Spree::Price.new(variant_id: id, currency: currency)
end
def amount_in(currency)
price_in(currency).try(:amount)
end
def price_modifier_amount_in(currency, options = {})
return 0 unless options.present?
options.keys.map { |key|
m = "#{key}_price_modifier_amount_in".to_sym
if self.respond_to? m
self.send(m, currency, options[key])
else
0
end
}.sum
end
def price_modifier_amount(options = {})
return 0 unless options.present?
options.keys.map { |key|
m = "#{key}_price_modifier_amount".to_sym
if self.respond_to? m
self.send(m, options[key])
else
0
end
}.sum
end
def name_and_sku
"#{name} - #{sku}"
end
def sku_and_options_text
"#{sku} #{options_text}".strip
end
def in_stock?
Rails.cache.fetch(in_stock_cache_key) do
total_on_hand > 0
end
end
def can_supply?(quantity=1)
Spree::Stock::Quantifier.new(self).can_supply?(quantity)
end
def total_on_hand
Spree::Stock::Quantifier.new(self).total_on_hand
end
# Shortcut method to determine if inventory tracking is enabled for this variant
# This considers both variant tracking flag and site-wide inventory tracking settings
def should_track_inventory?
self.track_inventory? && Spree::Config.track_inventory_levels
end
def track_inventory
self.should_track_inventory?
end
def volume
(width || 0) * (height || 0) * (depth || 0)
end
def dimension
(width || 0) + (height || 0) + (depth || 0)
end
private
def set_master_out_of_stock
if product.master && product.master.in_stock?
product.master.stock_items.update_all(:backorderable => false)
product.master.stock_items.each { |item| item.reduce_count_on_hand_to_zero }
end
end
# Ensures a new variant takes the product master price when price is not supplied
def check_price
if price.nil? && Spree::Config[:require_master_price]
raise 'No master variant found to infer price' unless (product && product.master)
raise 'Must supply price for variant or master.price for product.' if self == product.master
self.price = product.master.price
end
if currency.nil?
self.currency = Spree::Config[:currency]
end
end
def set_cost_currency
self.cost_currency = Spree::Config[:currency] if cost_currency.nil? || cost_currency.empty?
end
def create_stock_items
StockLocation.where(propagate_all_variants: true).each do |stock_location|
stock_location.propagate_variant(self)
end
end
def in_stock_cache_key
"variant-#{id}-in_stock"
end
def clear_in_stock_cache
Rails.cache.delete(in_stock_cache_key)
end
end
end