Skip to content

Instantly share code, notes, and snippets.

@kirylrb
Created July 13, 2016 16:47
Show Gist options
  • Save kirylrb/847eef05dce4740ca08b28922346a9f5 to your computer and use it in GitHub Desktop.
Save kirylrb/847eef05dce4740ca08b28922346a9f5 to your computer and use it in GitHub Desktop.
Product model with Roo importing
require 'csv'
require 'roo'
require 'globalize'
module Shoppe
class Product < ActiveRecord::Base
self.table_name = 'shoppe_products'
# Add dependencies for products
require_dependency 'shoppe/product/product_attributes'
require_dependency 'shoppe/product/variants'
# Attachments for this product
has_many :attachments, as: :parent, dependent: :destroy, autosave: true, class_name: 'Shoppe::Attachment'
# The product's categorizations
#
# @return [Shoppe::ProductCategorization]
has_many :product_categorizations, dependent: :destroy, class_name: 'Shoppe::ProductCategorization', inverse_of: :product
# The product's categories
#
# @return [Shoppe::ProductCategory]
has_many :product_categories, class_name: 'Shoppe::ProductCategory', through: :product_categorizations
# The product's tax rate
#
# @return [Shoppe::TaxRate]
belongs_to :tax_rate, class_name: 'Shoppe::TaxRate'
# Ordered items which are associated with this product
has_many :order_items, dependent: :restrict_with_exception, class_name: 'Shoppe::OrderItem', as: :ordered_item
# Orders which have ordered this product
has_many :orders, through: :order_items, class_name: 'Shoppe::Order'
# Stock level adjustments for this product
has_many :stock_level_adjustments, dependent: :destroy, class_name: 'Shoppe::StockLevelAdjustment', as: :item
# Validations
with_options if: proc { |p| p.parent.nil? } do |product|
# product.validate :has_at_least_one_product_category
product.validates :description, presence: true
product.validates :short_description, presence: true
end
validates :name, presence: true
validates :permalink, presence: true, uniqueness: true, permalink: true
validates :sku, presence: true
validates :weight, numericality: true
validates :price, numericality: true
validates :cost_price, numericality: true, allow_blank: true
# Before validation, set the permalink if we don't already have one
before_validation { self.permalink = name.parameterize if permalink.blank? && name.is_a?(String) }
# All active products
scope :active, -> { where(active: true) }
# All featured products
scope :featured, -> { where(featured: true) }
# Localisations
translates :name, :permalink, :description, :short_description
scope :ordered, -> { includes(:translations).order(:name) }
def attachments=(attrs)
if attrs['default_image']['file'].present? then attachments.build(attrs['default_image']) end
if attrs['data_sheet']['file'].present? then attachments.build(attrs['data_sheet']) end
if attrs['extra']['file'].present? then attrs['extra']['file'].each { |attr| attachments.build(file: attr, parent_id: attrs['extra']['parent_id'], parent_type: attrs['extra']['parent_type']) } end
end
# Return the name of the product
#
# @return [String]
def full_name
parent ? "#{parent.name} (#{name})" : name
end
# Is this product orderable?
#
# @return [Boolean]
def orderable?
return false unless active?
return false if has_variants?
true
end
# The price for the product
#
# @return [BigDecimal]
def price
# self.default_variant ? self.default_variant.price : read_attribute(:price)
default_variant ? default_variant.price : read_attribute(:price)
end
# Is this product currently in stock?
#
# @return [Boolean]
def in_stock?
default_variant ? default_variant.in_stock? : (stock_control? ? stock > 0 : true)
end
# Return the total number of items currently in stock
#
# @return [Fixnum]
def stock
stock_level_adjustments.sum(:adjustment)
end
# Return the first product category
#
# @return [Shoppe::ProductCategory]
def product_category
product_categories.first
rescue
nil
end
# Return attachment for the default_image role
#
# @return [String]
def default_image
attachments.for('default_image')
end
# Set attachment for the default_image role
def default_image_file=(file)
attachments.build(file: file, role: 'default_image')
end
# Return attachment for the data_sheet role
#
# @return [String]
def data_sheet
attachments.for('data_sheet')
end
# Search for products which include the given attributes and return an active record
# scope of these products. Chainable with other scopes and with_attributes methods.
# For example:
#
# Shoppe::Product.active.with_attribute('Manufacturer', 'Apple').with_attribute('Model', ['Macbook', 'iPhone'])
#
# @return [Enumerable]
def self.with_attributes(key, values)
product_ids = Shoppe::ProductAttribute.searchable.where(key: key, value: values).pluck(:product_id).uniq
where(id: product_ids)
end
# File.extname(file.original_filename)
# Roo::Excel.new(file.path)
def self.import(file)
spreadsheet = open_spreadsheet(file)
spreadsheet.default_sheet = spreadsheet.sheets.first
header = spreadsheet.row(1)
(2..spreadsheet.last_row).each do |i|
row = Hash[[header, spreadsheet.row(i)].transpose]
# Don't import products where the name is blank
next if row['name'].nil?
if product = where(name: row['name']).take
# Dont import products with the same name but update quantities
qty = row['qty'].to_i
if qty > 0
product.stock_level_adjustments.create!(description: I18n.t('shoppe.import'), adjustment: qty)
end
else
# product = Shoppe::Product.find_or_initialize_by(permalink: row['url'])
product = Shoppe::Product.find_or_initialize_by(permalink: row['url'], name: row['name'])
product.name = row['name']
product.sku = row['item_code']
product.description = row['abstract']
product.short_description = row['info']
product.price = row['price'].nil? ? 0 : row['price']
product.product_categories << begin
Shoppe::ProductCategory.find_or_initialize_by(name: 'Imported')
end
product.save!
qty = row['qty'].to_i
if qty > 0
product.stock_level_adjustments.create!(description: I18n.t('shoppe.import'), adjustment: qty)
end
end
end
end
def self.open_spreadsheet(file)
case File.extname(file.original_filename)
when '.csv' then Roo::CSV.new(file.path, csv_options: {col_sep: ";", encoding: Encoding::UTF_8})
when '.xls' then Roo::Excel.new(file.path)
when '.xlsx' then Roo::Excelx.new(file.path)
else fail I18n.t('shoppe.imports.errors.unknown_format', filename: File.original_filename)
end
end
private
# Validates
def has_at_least_one_product_category
errors.add(:base, 'must add at least one product category') if product_categories.blank?
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment