solidus_import_products 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +19 -0
  3. data/README.md +71 -0
  4. data/app/controllers/spree/admin/product_imports_controller.rb +35 -0
  5. data/app/jobs/import_products_job.rb +14 -0
  6. data/app/models/solidus_import_products/parser.rb +15 -0
  7. data/app/models/solidus_import_products/parser/base.rb +24 -0
  8. data/app/models/solidus_import_products/parser/csv.rb +98 -0
  9. data/app/models/spree/product_import.rb +82 -0
  10. data/app/overrides/add_import_to_admin_sidebar_menu.rb +6 -0
  11. data/app/services/solidus_import_products/create_variant.rb +109 -0
  12. data/app/services/solidus_import_products/import.rb +38 -0
  13. data/app/services/solidus_import_products/process_row.rb +93 -0
  14. data/app/services/solidus_import_products/save_product.rb +41 -0
  15. data/app/services/solidus_import_products/save_properties.rb +23 -0
  16. data/app/services/solidus_import_products/update_product.rb +45 -0
  17. data/app/views/spree/admin/product_imports/index.html.erb +71 -0
  18. data/app/views/spree/admin/product_imports/show.html.erb +37 -0
  19. data/app/views/spree/admin/shared/_import_sidebar_menu.erb +3 -0
  20. data/app/views/spree/user_mailer/product_import_results.text.erb +23 -0
  21. data/lib/generators/solidus_import_products/install/install_generator.rb +41 -0
  22. data/lib/generators/solidus_import_products/install/templates/config/initializers/solidus_import_product_settings.rb +10 -0
  23. data/lib/solidus_import_products.rb +8 -0
  24. data/lib/solidus_import_products/engine.rb +26 -0
  25. data/lib/solidus_import_products/exception.rb +19 -0
  26. data/lib/solidus_import_products/import_helper.rb +118 -0
  27. data/lib/solidus_import_products/logger.rb +15 -0
  28. data/lib/solidus_import_products/user_mailer_ext.rb +15 -0
  29. data/lib/tasks/solidus_import_products.rake +1 -0
  30. metadata +255 -0
@@ -0,0 +1,38 @@
1
+ module SolidusImportProducts
2
+ class Import
3
+ attr_accessor :product_imports, :logger
4
+
5
+ def initialize(args = { product_imports: nil })
6
+ self.product_imports = args[:product_imports]
7
+ self.logger = SolidusImportProducts::Logger.instance
8
+ end
9
+
10
+ def self.call(options = {})
11
+ new(options).call
12
+ end
13
+
14
+ def call
15
+ skus_of_products_before_import = Spree::Product.all.map(&:sku)
16
+ parser = product_imports.parse
17
+ col = parser.column_mappings
18
+
19
+ product_imports.start
20
+ ActiveRecord::Base.transaction do
21
+ parser.data_rows.each do |row|
22
+ SolidusImportProducts::ProcessRow.call(
23
+ parser: parser,
24
+ product_imports: product_imports,
25
+ row: row,
26
+ col: col,
27
+ skus_of_products_before_import: skus_of_products_before_import
28
+ )
29
+ end
30
+ end
31
+
32
+ product_imports.complete
33
+ rescue SolidusImportProducts::Exception::Base => e
34
+ product_imports.failure!
35
+ raise e
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,93 @@
1
+ module SolidusImportProducts
2
+ class ProcessRow
3
+ attr_accessor :parser, :product_imports, :logger, :row, :col, :product_information, :variant_field, :skus_of_products_before_import
4
+
5
+ VARIANT_FIELD_NAME = :name
6
+
7
+ def initialize(args = { parser: nil, product_imports: nil, row: nil, col: nil, skus_of_products_before_import: nil })
8
+ self.parser = args[:parser]
9
+ self.product_imports = args[:product_imports]
10
+ self.row = args[:row]
11
+ self.col = args[:col]
12
+ self.variant_field = VARIANT_FIELD_NAME
13
+ self.skus_of_products_before_import = args[:skus_of_products_before_import]
14
+ self.logger = SolidusImportProducts::Logger.instance
15
+ self.product_information = { variant_options: {}, images: [], variant_images: [], product_properties: {}, attributes: {} }
16
+ end
17
+
18
+ def self.call(options = {})
19
+ new(options).call
20
+ end
21
+
22
+ def call
23
+ extract_product_information
24
+ product_information_default_values
25
+ logger.log(product_information.to_s, :debug)
26
+
27
+ variant_column = col[variant_field]
28
+ product = Spree::Product.find_by(variant_field.to_s => row[variant_column])
29
+
30
+ unless product
31
+ if skus_of_products_before_import.include?(product_information[:attributes][:sku])
32
+ raise SolidusImportProducts::Exception::ProductError, "SKU #{product_information[:attributes][:sku]} exists, but #{variant_field}: #{row[variant_column]} not exists!! "
33
+ end
34
+ product = Spree::Product.new
35
+ end
36
+
37
+ unless product_imports.product?(product)
38
+ create_or_update_product(product)
39
+ product_imports.add_product(product)
40
+ end
41
+
42
+ SolidusImportProducts::CreateVariant.call(product: product, product_information: product_information)
43
+ end
44
+
45
+ private
46
+
47
+ def create_or_update_product(product)
48
+ properties_hash = SolidusImportProducts::UpdateProduct.call(product: product, product_information: product_information)
49
+ SolidusImportProducts::SaveProduct.call(product: product, product_information: product_information)
50
+ SolidusImportProducts::SaveProperties.call(product: product, properties_hash: properties_hash)
51
+ end
52
+
53
+ def extract_product_information
54
+ col.each do |key, value|
55
+ row[value].try :strip!
56
+ if parser.variant_option_field?(key)
57
+ product_information[:variant_options][key] = row[value]
58
+ elsif parser.property_field?(key)
59
+ product_information[:product_properties][key] = row[value]
60
+ elsif parser.image_field?(key)
61
+ product_information[:images].push(row[value])
62
+ elsif parser.variant_image_field?(key)
63
+ product_information[:variant_images].push(row[value])
64
+ else
65
+ product_information[:attributes][key] = key.to_s.eql?('price') ? convert_to_price(row[value]) : row[value]
66
+ end
67
+ end
68
+ end
69
+
70
+ def product_information_default_values
71
+ product_information[:attributes][:available_on] = Time.zone.today - 1.day if product_information[:attributes][:available_on].nil?
72
+
73
+ if product_information[:attributes][:shipping_category_id].nil?
74
+ sc = Spree::ShippingCategory.first
75
+ product_information[:attributes][:shipping_category_id] = sc.id if sc
76
+ end
77
+
78
+ product_information[:attributes][:retail_only] = 0 if product_information[:attributes][:retail_only].nil?
79
+ end
80
+
81
+ # Special process of prices because of locales and different decimal separator characters.
82
+ # We want to get a format with dot as decimal separator and without thousand separator
83
+ def convert_to_price(price_str)
84
+ raise SolidusImportProducts::Exception::InvalidPrice unless price_str
85
+ punt = price_str.index('.')
86
+ coma = price_str.index(',')
87
+ if !coma.nil? && !punt.nil?
88
+ price_str.gsub!(punt < coma ? '.' : ',', '')
89
+ end
90
+ price_str.tr(',', '.').to_f
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,41 @@
1
+ module SolidusImportProducts
2
+ class SaveProduct
3
+ attr_accessor :product, :product_information, :logger
4
+
5
+ include SolidusImportProducts::ImportHelper
6
+
7
+ def self.call(options = {})
8
+ new.call(options)
9
+ end
10
+
11
+ def call(args = { product: nil, product_information: nil })
12
+ self.logger = SolidusImportProducts::Logger.instance
13
+ self.product_information = args[:product_information]
14
+ self.product = args[:product]
15
+
16
+ logger.log("SAVE PRODUCT: #{product.inspect}", :debug)
17
+
18
+ unless product.valid?
19
+ msg = "A product could not be imported - here is the information we have:\n" \
20
+ "#{product_information}, #{product.inspect} #{product.errors.full_messages.join(', ')}"
21
+ logger.log(msg, :error)
22
+ raise SolidusImportProducts::Exception::ProductError, msg
23
+ end
24
+
25
+ product.save
26
+
27
+ # Associate our new product with any taxonomies that we need to worry about
28
+ if product_information[:attributes].key?(:taxonomies) && product_information[:attributes][:taxonomies]
29
+ associate_product_with_taxon(product, product_information[:attributes][:taxonomies], true)
30
+ end
31
+
32
+ # Finally, attach any images that have been specified
33
+ product_information[:images].each do |filename|
34
+ find_and_attach_image_to(product, filename)
35
+ end
36
+
37
+ logger.log("#{product.name} successfully imported.\n")
38
+ true
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,23 @@
1
+ module SolidusImportProducts
2
+ class SaveProperties
3
+ attr_accessor :product, :properties_hash, :logger
4
+
5
+ def self.call(options = {})
6
+ new.call(options)
7
+ end
8
+
9
+ def call(args = { product: nil, properties_hash: nil })
10
+ self.logger = SolidusImportProducts::Logger.instance
11
+ self.properties_hash = args[:properties_hash]
12
+ self.product = args[:product]
13
+
14
+ properties_hash.each do |field, value|
15
+ property = Spree::Property.where('lower(name) = ?', field).first
16
+ next unless property
17
+ product_property = Spree::ProductProperty.where(product_id: product.id, property_id: property.id).first_or_initialize
18
+ product_property.value = value
19
+ product_property.save!
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,45 @@
1
+ module SolidusImportProducts
2
+ class UpdateProduct
3
+ attr_accessor :product, :product_information, :logger
4
+
5
+ include SolidusImportProducts::ImportHelper
6
+
7
+ def self.call(options = {})
8
+ new.call(options)
9
+ end
10
+
11
+ def call(args = { product: nil, product_information: nil })
12
+ self.logger = SolidusImportProducts::Logger.instance
13
+ self.product_information = args[:product_information]
14
+ self.product = args[:product]
15
+
16
+ logger.log("UPDATE PRODUCT: #{product.inspect} #{product_information.inspect}", :debug)
17
+
18
+ product.update_attribute(:deleted_at, nil) if product.deleted_at
19
+ product.variants.each { |variant| variant.update_attribute(:deleted_at, nil) }
20
+
21
+ properties_hash = {}
22
+
23
+ product_information.each do |field, value|
24
+ if field == :product_properties
25
+ value.each { |prop_field, prop_value| properties_hash[prop_field] = prop_value }
26
+ elsif field == :attributes
27
+ value.each { |attr_field, attr_value| product.send("#{attr_field}=", attr_value) if product.respond_to?("#{attr_field}=") }
28
+ end
29
+ end
30
+
31
+ setup_shipping_category(product) unless product.shipping_category
32
+
33
+ properties_hash
34
+ end
35
+
36
+ private
37
+
38
+ def setup_shipping_category(product)
39
+ unless Spree::ShippingCategory.first
40
+ Spree::ShippingCategory.find_or_create_by(name: 'Default')
41
+ end
42
+ product.shipping_category = Spree::ShippingCategory.first
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,71 @@
1
+ <% content_for :page_title do %>
2
+ <%= t('form.product_import.heading') %>
3
+ <% end %>
4
+
5
+ <div class="row">
6
+ <div class="col-sm-6">
7
+ <%= form_for([:admin, @product_import], :method => :post, :html => { :multipart => true }) do |f| %>
8
+ <fieldset class="no-border-top">
9
+ <%= f.field_container :data_file, class: ['form-group'] do %>
10
+ <%= f.label :data_file, t('form.product_import.new.data_file') %><br />
11
+ <%= f.file_field :data_file %>
12
+ <%= f.error_message_on :data_file %>
13
+ <% end %>
14
+ <div class="field">
15
+ <%= f.label :separatorChar, t('form.product_import.separator_char') %><br />
16
+ <%= f.text_field :separatorChar, class: 'form-control required',:id => "separatorChar",:value => ";" ,:size => 1, :maxlength => 1 %>
17
+ </div>
18
+ <div class="field">
19
+ <%= f.label :encoding_csv, t('form.product_import.encoding') %><br />
20
+ <%= f.select(:encoding_csv,
21
+ options_from_collection_for_select(Spree::ProductImport::ENCODINGS, :to_s, :to_s, 'UTF-8'),
22
+ {:class => 'select2', :prompt => false}) %>
23
+ </div>
24
+ <div data-hook="buttons" class="form-actions">
25
+ <%= button Spree.t('actions.import'), 'ok', 'submit', {class: 'btn-success'} %>
26
+ </div>
27
+ </fieldset>
28
+
29
+ <% end %>
30
+ </div>
31
+ </div>
32
+ <table class="table">
33
+ <colgroup>
34
+ <col style="width: 15%">
35
+ <col style="width: 20%">
36
+ <col style="width: 10%">
37
+ <col style="width: 15%">
38
+ <col style="width: 10%">
39
+ <col style="width: 20%">
40
+ <col style="width: 5%">
41
+ </colgroup>
42
+ <thead>
43
+ <tr>
44
+ <th>Date creation</th>
45
+ <th>CSV Name</th>
46
+ <th>Status</th>
47
+ <th>Date Status</th>
48
+ <th>Imported</th>
49
+ <th>Error Message</th>
50
+ <th class="actions">Actions</th>
51
+ </tr>
52
+ </thead>
53
+ <tbody>
54
+ <% Spree::ProductImport.order("created_at DESC").all.each do |import| %>
55
+ <tr class="<%= cycle('odd', 'even') %>" id="<%= dom_id import %>">
56
+ <td><%= I18n.l(import.created_at.to_datetime,format: :short)%></td>
57
+ <td><%= link_to import.data_file_file_name, import.data_file.url -%></td>
58
+ <td><span class="label-<%= import.state.downcase %>"><%= t(import.state, :scope => "product_import.state")%></span></td>
59
+ <td><%= I18n.l(import.state_datetime.to_datetime,format: :short) %></td>
60
+ <td><%= import.product_ids.size -%></td>
61
+ <td><%= import.error_message%></td>
62
+ <td class="actions text-center">
63
+ <%= link_to_edit import, :url => admin_product_import_path(import), :no_text => true -%>
64
+ </td>
65
+ <td class="actions text-center">
66
+ <%= link_to_delete import, :url => admin_product_import_path(import), :no_text => true -%>
67
+ </td>
68
+ </tr>
69
+ <% end %>
70
+ </tbody>
71
+ </table>
@@ -0,0 +1,37 @@
1
+ <% content_for :page_title do %>
2
+ <%= t(:show_product_import) %>
3
+ <% end %>
4
+
5
+ <% content_for :page_actions do %>
6
+ <%= button_link_to t(:back_to_product_imports), admin_product_imports_path, :icon => 'icon-arrow-left' %>
7
+ <%= button_link_to t(:delete_product_import), admin_product_import_path(@product_import), {:icon => 'icon-remove', :method => :delete, :confirm => t("confirm_delete_product_import")} %>
8
+ <% end %>
9
+
10
+ <table class="table" id="listing_products">
11
+ <colgroup>
12
+ <col style="width: 20%">
13
+ <col style="width: 50%">
14
+ <col style="width: 20%">
15
+ <col style="width: 10%">
16
+ </colgroup>
17
+ <thead>
18
+ <tr data-hook="admin_products_index_headers">
19
+ <th><%= t(:sku) %></th>
20
+ <th><%= t(:name) %></th>
21
+ <th><%= t(:master_price) %></th>
22
+ <th class="actions" data-hook="admin_products_index_header_actions"></th>
23
+ </tr>
24
+ </thead>
25
+ <tbody>
26
+ <% @products.each do |product| %>
27
+ <tr class="<%= cycle('odd', 'even') %>" <%= "style='color: red;'" if product.deleted? %> id="<%= dom_id product %>" data-hook="admin_products_index_rows">
28
+ <td><%= product.sku rescue '' %></td>
29
+ <td><%= product.name rescue '' %></td>
30
+ <td><%= number_to_currency product.price rescue '' %></td>
31
+ <td class="actions" data-hook="admin_products_index_row_actions">
32
+ <%= link_to_edit_url edit_admin_product_path(product), :no_text => true unless product.deleted? %>
33
+ </td>
34
+ </tr>
35
+ <% end %>
36
+ </tbody>
37
+ </table>
@@ -0,0 +1,3 @@
1
+ <% if can? :admin, Spree::ProductImport %>
2
+ <%= tab :product_imports, url: spree.admin_product_imports_url %>
3
+ <% end %>
@@ -0,0 +1,23 @@
1
+ Hello,
2
+
3
+ <% if @error_message %>
4
+ Something went wrong during the product import.
5
+ Common causes for this include:
6
+ - Badly formatted CSV
7
+ - Special characters within the file
8
+ - Incorrect data
9
+ - Application or database error
10
+
11
+ Please check your file and try again.
12
+ To help diagnose the problem, we have attached the log file for the imports.
13
+
14
+ --
15
+
16
+ <%= @error_message %>
17
+
18
+ <% else %>
19
+ This is a quick email to let you know that the product import you submitted has been completed.
20
+ You can now see the products you added visible in your store.
21
+ <% end %>
22
+
23
+
@@ -0,0 +1,41 @@
1
+ module SolidusImportProducts
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ class_option :auto_run_migrations, type: :boolean, default: false
5
+ class_option :auto_skip_migrations, type: :boolean, default: false
6
+
7
+ def self.source_paths
8
+ paths = superclass.source_paths
9
+ paths << File.expand_path('../templates', "../../#{__FILE__}")
10
+ paths << File.expand_path('../templates', "../#{__FILE__}")
11
+ paths << File.expand_path('../templates', __FILE__)
12
+ paths.flatten
13
+ end
14
+
15
+ def add_migrations
16
+ if !options[:auto_skip_migrations]
17
+ run 'bundle exec rake solidus_import_products:install:migrations'
18
+ else
19
+ puts 'Skipping rake solidus_import_products:install:migrations, don\'t forget to run it!'
20
+ end
21
+
22
+ end
23
+
24
+ def add_files
25
+ template 'config/initializers/solidus_import_product_settings.rb', 'config/initializers/solidus_import_product_settings.rb'
26
+ end
27
+
28
+ def run_migrations
29
+ run_migrations = options[:auto_skip_migrations] ||
30
+ options[:auto_run_migrations] ||
31
+ ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]'))
32
+
33
+ if run_migrations && !options[:auto_skip_migrations]
34
+ run 'bundle exec rake db:migrate'
35
+ else
36
+ puts 'Skipping rake db:migrate, don\'t forget to run it!'
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,10 @@
1
+ Spree::ProductImport.settings = {
2
+ num_prods_for_delayed: 20, # From this number of products, the process is executed in delayed_job. Under it is processed immediately.
3
+ create_missing_taxonomies: true,
4
+ product_image_path: "#{Rails.root}/lib/etc/product_data/product-images/", # The location of images on disk
5
+ log_to: File.join(Rails.root, '/log/', "import_products_#{Rails.env}.log"), # Where to log to
6
+ destroy_original_products: false, # Disabled #Delete the products originally in the database after the import?
7
+ create_variants: true, # Compares products and creates a variant if that product already exists.
8
+ store_field: :store_code, # Which field of the column mappings contains either the store id or store code?
9
+ transaction: true # import product in a sql transaction so we can rollback when an exception is raised
10
+ }
@@ -0,0 +1,8 @@
1
+ require 'solidus_core'
2
+ require 'solidus_support'
3
+ require 'solidus_auth_devise'
4
+ require 'deface'
5
+ require 'solidus_import_products/exception'
6
+ require 'solidus_import_products/logger'
7
+ require 'solidus_import_products/import_helper'
8
+ require 'solidus_import_products/engine'