Class: Spree::Variant
- Inherits:
-
Object
- Object
- Spree::Variant
- Includes:
- DefaultPrice, MemoizedData, Metadata, Metafields, Searchable, Webhooks
- Defined in:
- app/models/spree/variant.rb,
app/models/spree/variant/webhooks.rb
Defined Under Namespace
Modules: Webhooks
Constant Summary collapse
- MEMOIZED_METHODS =
%w(in_stock on_sale backorderable tax_category options_text compare_at_price)
- DIMENSION_UNITS =
%w[mm cm in ft]
- WEIGHT_UNITS =
%w[g kg lb oz]
- LOCALIZED_NUMBERS =
FIXME: cost price should be represented with DisplayMoney class
%w(cost_price weight depth width height)
Constants included from DefaultPrice
Class Method Summary collapse
- .product_name_or_sku_cont(query) ⇒ Object
-
.search(query) ⇒ Object
Free-text variant search: SKU, parent product name, and any option-value presentation (e.g. "Red", "XL").
- .search_by_product_name_or_sku(query) ⇒ Object
Instance Method Summary collapse
-
#additional_images ⇒ Array<Spree::Image>
Returns all images except the default image, combining variant and product images.
-
#amount_in(currency) ⇒ BigDecimal
Returns the amount for the given currency.
-
#available? ⇒ Boolean
Returns true if the variant is available.
-
#backorderable? ⇒ Boolean
(also: #is_backorderable?)
Returns true if the variant is backorderable.
- #backordered? ⇒ Boolean
-
#compare_at_amount_in(currency) ⇒ BigDecimal
Returns the compare at amount for the given currency.
-
#compare_at_price ⇒ BigDecimal
Returns the compare at price of the variant.
-
#default_image ⇒ Object
deprecated
Deprecated.
Use #primary_media instead.
- #default_stock_location ⇒ Object
-
#descriptive_name ⇒ String
Returns the descriptive name of the variant.
-
#digital? ⇒ Boolean
Is this variant purely digital? (no physical product).
- #dimension ⇒ Object
- #discontinue! ⇒ Object
- #discontinued? ⇒ Boolean
-
#exchange_name ⇒ String
Returns the exchange name of the variant.
-
#find_option_value(opt_name) ⇒ Spree::OptionValue
Returns the option value for the given option name.
-
#gallery_media ⇒ ActiveRecord::Relation
Returns the variant's media gallery.
-
#has_associated_media? ⇒ Boolean
True if any product-level media is linked to this variant.
-
#has_media? ⇒ Boolean
(also: #has_images?)
Returns true if the variant has media (linked product-level or direct images).
-
#human_name ⇒ String
Returns the human name of the variant.
-
#in_stock? ⇒ Boolean
Returns true if the variant is in stock.
-
#in_stock_or_backorderable? ⇒ Boolean
Returns true if the variant is in stock or backorderable.
- #on_sale?(currency) ⇒ Boolean
-
#option_value(option_type) ⇒ String
Returns the presentation of the option value for the given option type.
-
#options ⇒ Array<Hash>
Returns an array of hashes with the option type name, value and presentation.
-
#options=(options = {}) ⇒ void
Sets the option values for the variant.
-
#options_text ⇒ String
Returns the options text of the variant.
-
#oversellable_now? ⇒ Boolean
Whether the variant can currently be bought by overselling — via backorder or pre-order.
-
#preorder? ⇒ Boolean
Returns true if the variant is sold as a pre-order: the product is active and not deleted, the variant is flagged preorderable and not discontinued, and the "ships by" date is open-ended or still in the future.
-
#price_for(context_or_options) ⇒ Spree::Price
Returns the price for the given context or options.
-
#price_in(currency) ⇒ Spree::Price
Returns the base price (global price, not from a price list) for the given currency.
-
#price_modifier_amount(options = {}) ⇒ BigDecimal
Returns the price modifier amount of the variant.
- #price_modifier_amount_in(currency, options = {}) ⇒ Object
-
#prices=(prices_params) ⇒ void
Syncs base prices from an array of hashes.
-
#primary_image ⇒ Spree::Image?
deprecated
Deprecated.
Use #primary_media instead.
- #purchasable? ⇒ Boolean
-
#secondary_image ⇒ Spree::Image?
Returns second Image for Variant (for hover effects).
-
#set_option_value(opt_name, opt_value, opt_type_position = nil) ⇒ void
Sets the option value for the given option name.
-
#set_price(currency, amount, compare_at_amount = nil) ⇒ void
Sets the base price (global price, not for a price list) for the given currency.
-
#set_stock(count_on_hand, backorderable = nil, stock_location = nil) ⇒ void
Sets the stock for the variant at a given location.
-
#should_track_inventory? ⇒ Boolean
Shortcut method to determine if inventory tracking is enabled for this variant This considers both variant tracking flag and site-wide inventory tracking settings.
-
#stock_items=(stock_items_params) ⇒ void
Syncs stock items from an array of hashes.
-
#tax_category ⇒ Spree::TaxCategory
Returns tax category for Variant.
-
#tax_category_id ⇒ Integer
Returns tax category ID for Variant.
-
#update_thumbnail! ⇒ Object
Updates primary_media_id to the first media item by position.
- #volume ⇒ Object
-
#weight_unit ⇒ String
Returns the weight unit for the variant.
- #with_digital_assets? ⇒ Boolean
Methods included from Metadata
#metadata, #metadata=, #public_metadata=
Class Method Details
.product_name_or_sku_cont(query) ⇒ Object
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'app/models/spree/variant.rb', line 205 def self.product_name_or_sku_cont(query) sanitized_query = ActiveRecord::Base.sanitize_sql_like(query.to_s.downcase.strip) query_pattern = "%#{sanitized_query}%" sku_condition = arel_table[:sku].lower.matches(query_pattern) if Spree.use_translations? translation_arel_table = Product::Translation.arel_table.alias(Product.translation_table_alias)[:name] product_name_condition = translation_arel_table.lower.matches(query_pattern) joins(:product). join_translation_table(Product). where(product_name_condition.or(sku_condition)) else product_name_condition = Product.arel_table[:name].lower.matches(query_pattern) joins(:product).where(product_name_condition.or(sku_condition)) end end |
.search(query) ⇒ Object
Free-text variant search: SKU, parent product name, and any option-value presentation (e.g. "Red", "XL"). The 3-char floor keeps single-letter queries from triggering a full scan.
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'app/models/spree/variant.rb', line 149 def self.search(query) return none if query.blank? || query.length < 3 conditions = [ search_condition(self, :sku, query), search_condition(Spree::OptionValue, :presentation, query), ] if Spree.use_translations? translation_table = Product::Translation.arel_table.alias(Product.translation_table_alias) sanitized = sanitize_query_for_search(query) conditions << translation_table[:name].lower.matches("%#{sanitized}%", '\\') else conditions << search_condition(Spree::Product, :name, query) end relation = joins(:product).left_joins(:option_values) relation = relation.join_translation_table(Product) if Spree.use_translations? relation.where(conditions.reduce(:or)).distinct end |
.search_by_product_name_or_sku(query) ⇒ Object
223 224 225 |
# File 'app/models/spree/variant.rb', line 223 def self.search_by_product_name_or_sku(query) product_name_or_sku_cont(query) end |
Instance Method Details
#additional_images ⇒ Array<Spree::Image>
Returns all images except the default image, combining variant and product images.
369 370 371 |
# File 'app/models/spree/variant.rb', line 369 def additional_images @additional_images ||= (images + product.images).uniq.reject { |image| image.id == default_image&.id } end |
#amount_in(currency) ⇒ BigDecimal
Returns the amount for the given currency.
495 496 497 |
# File 'app/models/spree/variant.rb', line 495 def amount_in(currency) price_in(currency).try(:amount) end |
#available? ⇒ Boolean
Returns true if the variant is available.
239 240 241 |
# File 'app/models/spree/variant.rb', line 239 def available? !discontinued? && product.available? end |
#backorderable? ⇒ Boolean Also known as: is_backorderable?
Returns true if the variant is backorderable.
652 653 654 |
# File 'app/models/spree/variant.rb', line 652 def backorderable? @backorderable ||= quantifier.backorderable? end |
#backordered? ⇒ Boolean
708 709 710 |
# File 'app/models/spree/variant.rb', line 708 def backordered? @backordered ||= !in_stock? && stock_items.exists?(backorderable: true) end |
#compare_at_amount_in(currency) ⇒ BigDecimal
Returns the compare at amount for the given currency.
502 503 504 |
# File 'app/models/spree/variant.rb', line 502 def compare_at_amount_in(currency) price_in(currency).try(:compare_at_amount) end |
#compare_at_price ⇒ BigDecimal
Returns the compare at price of the variant.
640 641 642 |
# File 'app/models/spree/variant.rb', line 640 def compare_at_price @compare_at_price ||= price_in(cost_currency).try(:compare_at_amount) end |
#default_image ⇒ Object
Use #primary_media instead.
339 340 341 342 |
# File 'app/models/spree/variant.rb', line 339 def default_image Spree::Deprecation.warn('Spree::Variant#default_image is deprecated and will be removed in Spree 6.0. Please use Spree::Variant#primary_media instead.') primary_media end |
#default_stock_location ⇒ Object
605 606 607 |
# File 'app/models/spree/variant.rb', line 605 def default_stock_location Spree::Store.current.default_stock_location end |
#descriptive_name ⇒ String
Returns the descriptive name of the variant.
304 305 306 |
# File 'app/models/spree/variant.rb', line 304 def descriptive_name is_master? ? name + ' - Master' : name + ' - ' + end |
#digital? ⇒ Boolean
Is this variant purely digital? (no physical product)
715 716 717 |
# File 'app/models/spree/variant.rb', line 715 def digital? product.digital? end |
#dimension ⇒ Object
690 691 692 |
# File 'app/models/spree/variant.rb', line 690 def dimension (width || 0) + (height || 0) + (depth || 0) end |
#discontinue! ⇒ Object
700 701 702 |
# File 'app/models/spree/variant.rb', line 700 def discontinue! update_attribute(:discontinue_on, Time.current) end |
#discontinued? ⇒ Boolean
704 705 706 |
# File 'app/models/spree/variant.rb', line 704 def discontinued? !!discontinue_on && discontinue_on <= Time.current end |
#exchange_name ⇒ String
Returns the exchange name of the variant.
298 299 300 |
# File 'app/models/spree/variant.rb', line 298 def exchange_name is_master? ? name : end |
#find_option_value(opt_name) ⇒ Spree::OptionValue
Returns the option value for the given option name.
449 450 451 |
# File 'app/models/spree/variant.rb', line 449 def find_option_value(opt_name) option_values.includes(:option_type).detect { |o| o.option_type.name.parameterize == opt_name.parameterize } end |
#gallery_media ⇒ ActiveRecord::Relation
Returns the variant's media gallery. Prefers product-level media linked via variant_media (5.5+) — these reuse a single blob across variants. Falls back to direct variant images for legacy uploads.
313 314 315 316 317 |
# File 'app/models/spree/variant.rb', line 313 def gallery_media return associated_media if has_associated_media? images end |
#has_associated_media? ⇒ Boolean
Returns true if any product-level media is linked to this variant.
332 333 334 335 336 |
# File 'app/models/spree/variant.rb', line 332 def has_associated_media? return variant_media.any? if variant_media.loaded? variant_media.exists? end |
#has_media? ⇒ Boolean Also known as: has_images?
Returns true if the variant has media (linked product-level or direct images). Uses loaded associations when available, otherwise falls back to counter cache.
322 323 324 325 326 327 |
# File 'app/models/spree/variant.rb', line 322 def has_media? return true if has_associated_media? return images.any? if images.loaded? media_count.positive? end |
#human_name ⇒ String
Returns the human name of the variant.
229 230 231 232 233 234 235 |
# File 'app/models/spree/variant.rb', line 229 def human_name @human_name ||= option_values. joins(option_type: :product_option_types). merge(product.product_option_types). reorder('spree_product_option_types.position'). pluck(:presentation).join('/') end |
#in_stock? ⇒ Boolean
Returns true if the variant is in stock.
646 647 648 |
# File 'app/models/spree/variant.rb', line 646 def in_stock? @in_stock ||= total_on_hand.positive? end |
#in_stock_or_backorderable? ⇒ Boolean
Returns true if the variant is in stock or backorderable.
258 259 260 |
# File 'app/models/spree/variant.rb', line 258 def in_stock_or_backorderable? self.class.in_stock_or_backorderable.exists?(id: id) end |
#on_sale?(currency) ⇒ Boolean
656 657 658 |
# File 'app/models/spree/variant.rb', line 656 def on_sale?(currency) @on_sale ||= price_in(currency)&.discounted? end |
#option_value(option_type) ⇒ String
Returns the presentation of the option value for the given option type.
456 457 458 459 460 461 462 |
# File 'app/models/spree/variant.rb', line 456 def option_value(option_type) if option_type.is_a?(Spree::OptionType) option_values.detect { |o| o.option_type_id == option_type.id }.try(:presentation) else find_option_value(option_type).try(:presentation) end end |
#options ⇒ Array<Hash>
Returns an array of hashes with the option type name, value and presentation
375 376 377 378 379 380 381 382 383 384 385 386 387 |
# File 'app/models/spree/variant.rb', line 375 def @options ||= option_values. includes(option_type: :product_option_types). merge(product.product_option_types). reorder('spree_product_option_types.position'). map do |option_value| { name: option_value.option_type.name, value: option_value.name, presentation: option_value.presentation } end end |
#options=(options = {}) ⇒ void
This method returns an undefined value.
Sets the option values for the variant
392 393 394 395 396 397 398 399 400 401 402 403 |
# File 'app/models/spree/variant.rb', line 392 def ( = {}) if product.nil? @pending_options = return end .each do |option| next if option[:name].blank? || option[:value].blank? set_option_value(option[:name], option[:value], option[:position]) end end |
#options_text ⇒ String
Returns the options text of the variant.
284 285 286 287 288 289 290 291 292 293 294 |
# File 'app/models/spree/variant.rb', line 284 def @options_text ||= if option_values.loaded? option_values.sort_by do |ov| ov.option_type.position end.map { |ov| "#{ov.option_type.presentation}: #{ov.presentation}" }.to_sentence(words_connector: ', ', two_words_connector: ', ') else option_values.includes(:option_type).joins(:option_type).order("#{Spree::OptionType.table_name}.position").map do |ov| "#{ov.option_type.presentation}: #{ov.presentation}" end.to_sentence(words_connector: ', ', two_words_connector: ', ') end end |
#oversellable_now? ⇒ Boolean
Whether the variant can currently be bought by overselling — via
backorder or pre-order. Unlimited when backorder_limit is nil (empty =
no cap); with a limit, purchasable only while the oversell allowance
remains.
674 675 676 677 678 |
# File 'app/models/spree/variant.rb', line 674 def oversellable_now? return false unless backorderable? || preorder? backorder_limit.nil? || can_supply?(1) end |
#preorder? ⇒ Boolean
Returns true if the variant is sold as a pre-order: the product is active
and not deleted, the variant is flagged preorderable and not discontinued,
and the "ships by" date is open-ended or still in the future.
Pre-order relaxes only the publish-date embargo, not the rest of the
availability rules. Like backorder, a pre-order can make the variant
purchasable beyond on-hand stock; the oversell cap is backorder_limit
(empty ⇒ unlimited), and it adds the ship-by promise.
251 252 253 254 |
# File 'app/models/spree/variant.rb', line 251 def preorder? !discontinued? && preorderable? && product.active? && !product.deleted? && (preorder_ships_at.nil? || preorder_ships_at > Time.current) end |
#price_for(context_or_options) ⇒ Spree::Price
Returns the price for the given context or options.
579 580 581 582 583 584 585 586 587 588 589 |
# File 'app/models/spree/variant.rb', line 579 def price_for() context = if .is_a?(Spree::Pricing::Context) elsif .is_a?(Hash) Spree::Pricing::Context.new(**.merge(variant: self)) else raise ArgumentError, 'Must provide a Pricing::Context or options hash' end Spree::Pricing::Resolver.new(context).resolve end |
#price_in(currency) ⇒ Spree::Price
Returns the base price (global price, not from a price list) for the given currency. Use price_for(context) when you need to resolve prices including price lists.
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 |
# File 'app/models/spree/variant.rb', line 468 def price_in(currency) currency = currency&.upcase price = if prices.loaded? && prices.any? prices.detect { |p| p.currency == currency && p.price_list_id.nil? } else prices.base_prices.find_by(currency: currency) end if price.nil? return Spree::Price.new( currency: currency, variant_id: id ) end price rescue TypeError Spree::Price.new( currency: currency, variant_id: id ) end |
#price_modifier_amount(options = {}) ⇒ BigDecimal
Returns the price modifier amount of the variant.
625 626 627 628 629 630 631 632 633 634 635 636 |
# File 'app/models/spree/variant.rb', line 625 def price_modifier_amount( = {}) return 0 unless .present? .keys.map do |key| m = "#{key}_price_modifier_amount".to_sym if respond_to? m send(m, [key]) else 0 end end.sum end |
#price_modifier_amount_in(currency, options = {}) ⇒ Object
609 610 611 612 613 614 615 616 617 618 619 620 |
# File 'app/models/spree/variant.rb', line 609 def price_modifier_amount_in(currency, = {}) return 0 unless .present? .keys.map do |key| m = "#{key}_price_modifier_amount_in".to_sym if respond_to? m send(m, currency, [key]) else 0 end end.sum end |
#prices=(prices_params) ⇒ void
This method returns an undefined value.
Syncs base prices from an array of hashes.
Upserts prices for listed currencies, removes base prices for unlisted currencies.
On new records, builds prices in memory (saved when variant is saved).
On persisted records, saves prices immediately and removes unlisted currencies.
An empty array clears every base price — distinguished from nil (no
change requested), which falls through to the default ActiveRecord setter.
514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 |
# File 'app/models/spree/variant.rb', line 514 def prices=(prices_params) return super if prices_params.nil? || prices_params.first.is_a?(Spree::Price) currencies_in_payload = [] prices_params.each do |price_data| price_data = price_data.to_h.with_indifferent_access currencies_in_payload << price_data[:currency] set_price(price_data[:currency], price_data[:amount], price_data[:compare_at_amount]) end # Remove base prices for currencies not in the payload (including the # `prices_params == []` case, which clears every base price). prices.base_prices.where.not(currency: currencies_in_payload).destroy_all if persisted? end |
#primary_image ⇒ Spree::Image?
Use #primary_media instead.
Returns first Image for Variant.
356 357 358 359 |
# File 'app/models/spree/variant.rb', line 356 def primary_image Spree::Deprecation.warn('Spree::Variant#primary_image is deprecated and will be removed in Spree 6.0. Please use Spree::Variant#primary_media instead.') primary_media end |
#purchasable? ⇒ Boolean
664 665 666 |
# File 'app/models/spree/variant.rb', line 664 def purchasable? in_stock? || oversellable_now? end |
#secondary_image ⇒ Spree::Image?
Returns second Image for Variant (for hover effects).
363 364 365 |
# File 'app/models/spree/variant.rb', line 363 def secondary_image images.second end |
#set_option_value(opt_name, opt_value, opt_type_position = nil) ⇒ void
This method returns an undefined value.
Sets the option value for the given option name.
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 |
# File 'app/models/spree/variant.rb', line 410 def set_option_value(opt_name, opt_value, opt_type_position = nil) # no option values on master return if is_master option_type = Spree::OptionType.where(name: opt_name.parameterize).first_or_initialize do |o| o.name = o.presentation = opt_name o.save! end current_value = find_option_value(opt_name) if current_value.nil? # then we have to check to make sure that the product has the option type product_option_type = if (existing_prod_ot = product.product_option_types.find { |ot| ot.option_type_id == option_type.id }) existing_prod_ot else product_option_type = product.product_option_types.new product_option_type.option_type = option_type end product_option_type.position = opt_type_position if opt_type_position product_option_type.save! if product_option_type.new_record? || product_option_type.changed? else return if current_value.name.parameterize == opt_value.parameterize option_values.delete(current_value) end option_value = option_type.option_values.where(name: opt_value.parameterize).first_or_initialize do |o| o.name = o.presentation = opt_value o.save! end option_values << option_value save end |
#set_price(currency, amount, compare_at_amount = nil) ⇒ void
This method returns an undefined value.
Sets the base price (global price, not for a price list) for the given currency.
556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 |
# File 'app/models/spree/variant.rb', line 556 def set_price(currency, amount, compare_at_amount = nil) # When the prices association is already loaded (eager-loaded for # serialization), reuse the cached base-price instance so readers that # branch on `prices.loaded?` (Pricing::Resolver, #price_in, serializers) # observe the write without a reload. `base_prices.find_or_initialize_by` # would issue a fresh query and return a detached object, leaving the # loaded collection — and the serialized response — stale. price = if prices.loaded? prices.detect { |p| p.price_list_id.nil? && p.currency == currency } || prices.build(currency: currency) else prices.base_prices.find_or_initialize_by(currency: currency) end price.amount = amount price.compare_at_amount = compare_at_amount price.save! if persisted? end |
#set_stock(count_on_hand, backorderable = nil, stock_location = nil) ⇒ void
This method returns an undefined value.
Sets the stock for the variant at a given location. Mirrors set_price: find-or-initialize, set attrs, save only if persisted.
597 598 599 600 601 602 603 |
# File 'app/models/spree/variant.rb', line 597 def set_stock(count_on_hand, backorderable = nil, stock_location = nil) stock_location ||= default_stock_location stock_item = stock_items.find_or_initialize_by(stock_location: stock_location) stock_item.count_on_hand = count_on_hand stock_item.backorderable = backorderable if backorderable.present? stock_item.save! if persisted? end |
#should_track_inventory? ⇒ Boolean
Shortcut method to determine if inventory tracking is enabled for this variant This considers both variant tracking flag and site-wide inventory tracking settings
682 683 684 |
# File 'app/models/spree/variant.rb', line 682 def should_track_inventory? track_inventory? && Spree::Config.track_inventory_levels end |
#stock_items=(stock_items_params) ⇒ void
This method returns an undefined value.
Syncs stock items from an array of hashes. Upserts stock for listed locations, soft-deletes stock items for unlisted locations. On new records, defers to after_create callback.
535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 |
# File 'app/models/spree/variant.rb', line 535 def stock_items=(stock_items_params) return super if stock_items_params.blank? || stock_items_params.first.is_a?(Spree::StockItem) location_ids_in_payload = [] stock_items_params.each do |stock_data| stock_data = stock_data.to_h.with_indifferent_access location = Spree::StockLocation.find_by_param(stock_data[:stock_location_id]) location_ids_in_payload << location.id set_stock(stock_data[:count_on_hand], stock_data[:backorderable], location) end # Soft-delete stock items for locations not in the payload stock_items.where.not(stock_location_id: location_ids_in_payload).destroy_all if persisted? end |
#tax_category ⇒ Spree::TaxCategory
Returns tax category for Variant
264 265 266 267 268 269 270 |
# File 'app/models/spree/variant.rb', line 264 def tax_category @tax_category ||= if self[:tax_category_id].nil? product.tax_category else Spree::TaxCategory.find_by(id: self[:tax_category_id]) || product.tax_category end end |
#tax_category_id ⇒ Integer
Returns tax category ID for Variant
274 275 276 277 278 279 280 |
# File 'app/models/spree/variant.rb', line 274 def tax_category_id @tax_category_id ||= if self[:tax_category_id].nil? product.tax_category_id else self[:tax_category_id] end end |
#update_thumbnail! ⇒ Object
Updates primary_media_id to the first media item by position. Called when media is added, removed, or reordered. Uses gallery_media so product-level assets linked via VariantMedia are considered alongside legacy variant-pinned images.
348 349 350 351 |
# File 'app/models/spree/variant.rb', line 348 def update_thumbnail! first_media = gallery_media.first update_column(:primary_media_id, first_media&.id) end |
#volume ⇒ Object
686 687 688 |
# File 'app/models/spree/variant.rb', line 686 def volume (width || 0) * (height || 0) * (depth || 0) end |
#weight_unit ⇒ String
Returns the weight unit for the variant
696 697 698 |
# File 'app/models/spree/variant.rb', line 696 def weight_unit attributes['weight_unit'] || Spree::Store.default.preferred_weight_unit end |
#with_digital_assets? ⇒ Boolean
719 720 721 |
# File 'app/models/spree/variant.rb', line 719 def with_digital_assets? digitals.any? end |