workarea-magento_data_importer 1.0.0

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 (88) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  3. data/.github/ISSUE_TEMPLATE/documentation-request.md +17 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. data/.github/workflows/ci.yml +58 -0
  6. data/.gitignore +24 -0
  7. data/.rubocop.yml +3 -0
  8. data/CHANGELOG.md +17 -0
  9. data/Gemfile +16 -0
  10. data/LICENSE +52 -0
  11. data/README.md +89 -0
  12. data/Rakefile +56 -0
  13. data/app/assets/images/workarea/admin/magento_data_importer/.keep +0 -0
  14. data/app/assets/images/workarea/storefront/magento_data_importer/.keep +0 -0
  15. data/app/assets/javascripts/workarea/admin/magento_data_importer/.keep +0 -0
  16. data/app/assets/javascripts/workarea/storefront/magento_data_importer/.keep +0 -0
  17. data/app/assets/stylesheets/workarea/admin/magento_data_importer/.keep +0 -0
  18. data/app/assets/stylesheets/workarea/storefront/magento_data_importer/.keep +0 -0
  19. data/app/controllers/.keep +0 -0
  20. data/app/helpers/.keep +0 -0
  21. data/app/mailers/.keep +0 -0
  22. data/app/models/workarea/import/magento_product.rb +21 -0
  23. data/app/views/.keep +0 -0
  24. data/bin/rails +25 -0
  25. data/config/initializers/workarea.rb +3 -0
  26. data/config/routes.rb +2 -0
  27. data/lib/tasks/magento_import.rake +19 -0
  28. data/lib/workarea/magento_data_importer.rb +17 -0
  29. data/lib/workarea/magento_data_importer/base_product.rb +192 -0
  30. data/lib/workarea/magento_data_importer/configurable_product.rb +110 -0
  31. data/lib/workarea/magento_data_importer/engine.rb +10 -0
  32. data/lib/workarea/magento_data_importer/import_products.rb +86 -0
  33. data/lib/workarea/magento_data_importer/simple_product.rb +73 -0
  34. data/lib/workarea/magento_data_importer/taxonomy.rb +32 -0
  35. data/lib/workarea/magento_data_importer/version.rb +5 -0
  36. data/test/dummy/.ruby-version +1 -0
  37. data/test/dummy/Rakefile +6 -0
  38. data/test/dummy/app/assets/config/manifest.js +3 -0
  39. data/test/dummy/app/assets/images/.keep +0 -0
  40. data/test/dummy/app/assets/javascripts/application.js +14 -0
  41. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  42. data/test/dummy/app/controllers/application_controller.rb +2 -0
  43. data/test/dummy/app/controllers/concerns/.keep +0 -0
  44. data/test/dummy/app/helpers/application_helper.rb +2 -0
  45. data/test/dummy/app/jobs/application_job.rb +2 -0
  46. data/test/dummy/app/mailers/application_mailer.rb +4 -0
  47. data/test/dummy/app/models/concerns/.keep +0 -0
  48. data/test/dummy/app/views/layouts/application.html.erb +15 -0
  49. data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
  50. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  51. data/test/dummy/bin/bundle +3 -0
  52. data/test/dummy/bin/rails +4 -0
  53. data/test/dummy/bin/rake +4 -0
  54. data/test/dummy/bin/setup +28 -0
  55. data/test/dummy/bin/update +28 -0
  56. data/test/dummy/bin/yarn +11 -0
  57. data/test/dummy/config.ru +5 -0
  58. data/test/dummy/config/application.rb +34 -0
  59. data/test/dummy/config/boot.rb +5 -0
  60. data/test/dummy/config/environment.rb +5 -0
  61. data/test/dummy/config/environments/development.rb +52 -0
  62. data/test/dummy/config/environments/production.rb +83 -0
  63. data/test/dummy/config/environments/test.rb +45 -0
  64. data/test/dummy/config/initializers/application_controller_renderer.rb +8 -0
  65. data/test/dummy/config/initializers/assets.rb +14 -0
  66. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  67. data/test/dummy/config/initializers/content_security_policy.rb +25 -0
  68. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  69. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  70. data/test/dummy/config/initializers/inflections.rb +16 -0
  71. data/test/dummy/config/initializers/mime_types.rb +4 -0
  72. data/test/dummy/config/initializers/workarea.rb +5 -0
  73. data/test/dummy/config/initializers/wrap_parameters.rb +9 -0
  74. data/test/dummy/config/locales/en.yml +33 -0
  75. data/test/dummy/config/puma.rb +37 -0
  76. data/test/dummy/config/routes.rb +5 -0
  77. data/test/dummy/config/spring.rb +6 -0
  78. data/test/dummy/db/seeds.rb +2 -0
  79. data/test/dummy/lib/assets/.keep +0 -0
  80. data/test/dummy/log/.keep +0 -0
  81. data/test/dummy/package.json +5 -0
  82. data/test/factories/workarea/magento_data_file.rb +11 -0
  83. data/test/fixtures/magento_products.csv +28 -0
  84. data/test/lib/workarea/magento_data_importer/import_products_test.rb +72 -0
  85. data/test/teaspoon_env.rb +6 -0
  86. data/test/test_helper.rb +10 -0
  87. data/workarea-magento_data_importer.gemspec +20 -0
  88. metadata +143 -0
data/app/views/.keep ADDED
File without changes
data/bin/rails ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path('..', __dir__)
6
+ ENGINE_PATH = File.expand_path('../lib/workarea/magento_data_importer/engine', __dir__)
7
+ APP_PATH = File.expand_path('../test/dummy/config/application', __dir__)
8
+
9
+ # Set up gems listed in the Gemfile.
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
11
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
12
+
13
+ require "rails"
14
+ # Pick the frameworks you want:
15
+ require "active_model/railtie"
16
+ require "active_job/railtie"
17
+ # require "active_record/railtie"
18
+ # require "active_storage/engine"
19
+ require "action_controller/railtie"
20
+ require "action_mailer/railtie"
21
+ require "action_view/railtie"
22
+ # require "action_cable/engine"
23
+ require "sprockets/railtie"
24
+ require "rails/test_unit/railtie"
25
+ require 'rails/engine/commands'
@@ -0,0 +1,3 @@
1
+ Workarea.configure do |config|
2
+ # Add custom configuration here
3
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ Rails.application.routes.draw do
2
+ end
@@ -0,0 +1,19 @@
1
+ namespace :workarea do
2
+ namespace :magento do
3
+ desc 'Import Magento Catalog Data'
4
+ task import_products: :environment do
5
+ require 'workarea/seeds'
6
+
7
+ Workarea::Seeds.puts_with_color "== Starting Magento Products Migration", :yellow
8
+
9
+ raise 'No File provided' if ARGV.length < 2
10
+
11
+ file_path = ARGV.second
12
+
13
+ Workarea::MagentoDataImporter::ImportProducts.import!(file_path)
14
+
15
+ Workarea::Seeds.puts_with_color "\n== Updating Elasticsearch data", :yellow
16
+ Rake::Task['workarea:search_index:all'].invoke
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ require 'workarea'
2
+ require 'workarea/storefront'
3
+ require 'workarea/admin'
4
+
5
+ require 'workarea/magento_data_importer/engine'
6
+ require 'workarea/magento_data_importer/version'
7
+
8
+ require 'workarea/magento_data_importer/import_products'
9
+ require 'workarea/magento_data_importer/base_product'
10
+ require 'workarea/magento_data_importer/simple_product'
11
+ require 'workarea/magento_data_importer/configurable_product'
12
+ require 'workarea/magento_data_importer/taxonomy'
13
+
14
+ module Workarea
15
+ module MagentoDataImporter
16
+ end
17
+ end
@@ -0,0 +1,192 @@
1
+ module Workarea
2
+ module MagentoDataImporter
3
+ class BaseProduct
4
+ module ProductUrl
5
+ include Workarea::I18n::DefaultUrlOptions
6
+ include Storefront::Engine.routes.url_helpers
7
+ extend self
8
+ end
9
+
10
+ attr_reader :magento_product, :product_data, :product
11
+
12
+ def initialize(magento_product)
13
+ @magento_product = magento_product
14
+ @product_data = magento_product.product_data.deep_symbolize_keys
15
+ @product = Workarea::Catalog::Product.find_or_initialize_by(id: magento_product.magento_product_id)
16
+ end
17
+
18
+ private
19
+
20
+ # Converts the row's product data into a hash of data to be used in creating
21
+ # and updating the product information.
22
+ #
23
+ # @return [Hash]
24
+ def product_attributes
25
+ {
26
+ name: product_data[:name],
27
+ description: product_data[:description],
28
+ meta_description: product_data[:meta_description],
29
+ }
30
+ end
31
+
32
+ # Converts the row's product data into a hash of filters.
33
+ #
34
+ # @param attrs [Hash] the rows product data
35
+ # @return [Hash]
36
+ def row_filters(attrs)
37
+ return if ENV["product_filter_columns"].blank?
38
+ filter_columns = ENV["product_filter_columns"]
39
+ headers = filter_columns.split(",")
40
+
41
+ filters = {}
42
+
43
+ headers.each do |header|
44
+ k = header.to_sym
45
+ v = attrs[k]
46
+ if v.present?
47
+ filters[k] = v
48
+ end
49
+ end
50
+ filters
51
+ end
52
+
53
+ # Adds a hash to an existing set of product filters
54
+ #
55
+ # @param existing_filters [Hash] the set of existing product filters
56
+ # @param new_filters [Hash] new filters to add
57
+ # @return [Hash]
58
+ def add_filter_values(existing_filters, new_filters)
59
+ return existing_filters unless new_filters.present?
60
+
61
+ new_filters.each do |k, v|
62
+ if existing_filters.key?(k)
63
+ existing_filters[k] << v
64
+ existing_filters[k].uniq!
65
+ else
66
+ existing_filters[k] = [v]
67
+ end
68
+ end
69
+ existing_filters
70
+ end
71
+
72
+ # Creates a hash based on the product data's category value, data is split
73
+ # on "/" which is the default seperator in the magento export
74
+ #
75
+ # @return [Hash]
76
+ def category_filters(attrs)
77
+ if attrs[:_category].present?
78
+ { import_category: attrs[:_category].split('/') }
79
+ else
80
+ {}
81
+ end
82
+ end
83
+
84
+ # Creates a hash based on the configured product attributes columns
85
+ #
86
+ # @param attrs [Hash] a hash of magento product data
87
+ # @return [Hash]
88
+ def build_product_details(attrs)
89
+ return if ENV["product_attributes_columns"].blank?
90
+ details_columns = ENV["product_attributes_columns"]
91
+
92
+ headers = details_columns.split(",")
93
+
94
+ details = {}
95
+
96
+ headers.each do |header|
97
+ k = header.to_sym
98
+ v = attrs[k]
99
+ if v.present?
100
+ details[k] = v
101
+ end
102
+ end
103
+ details
104
+ end
105
+
106
+ # Creates a hash based on the configured product attributes columns
107
+ #
108
+ # @param attrs [Hash] a hash of magento product data
109
+ # @return [Hash]
110
+ def create_categories(attrs)
111
+ return unless attrs[:_category].present?
112
+ categories = attrs[:_category].split('/')
113
+
114
+ count = 0
115
+ max_size = categories.size - 1
116
+
117
+ while count <= max_size do
118
+ client_id = categories[0..count].join("/")
119
+ category = Workarea::Catalog::Category.find_or_initialize_by(client_id: client_id)
120
+
121
+ category_name = client_id.split("/").last
122
+ category.name = category_name.titleize
123
+
124
+ existing_rules = category.product_rules.map(&:value)
125
+ category.product_rules.build(name: "import_category", operator: "equals", value: category_name) unless existing_rules.include?(category_name)
126
+ category.save!
127
+
128
+ count = count + 1
129
+ end
130
+ end
131
+
132
+ # Creates a Workarea::Catalog::ProductImage based on a magento export row
133
+ #
134
+ # @param attrs [Hash] a hash of magento product data
135
+ # @return [Bool]
136
+ def build_image(attrs)
137
+ return unless ENV["product_image_base_url"].present? && attrs[:image].present?
138
+
139
+ return if attrs[:image] == "no_selection"
140
+
141
+ product_image_base_url = ENV["product_image_base_url"]
142
+ option_column = ENV["image_option_column"]
143
+
144
+ image_path = product_image_base_url + attrs[:image]
145
+
146
+ option = option_column.present? ? attrs[option_column.to_sym] : nil
147
+
148
+ image_attributes = {
149
+ position: attrs[:_media_position],
150
+ image_url: image_path,
151
+ option: option
152
+ }
153
+
154
+ existing_image = product.images.detect { |i| i.image_name == attrs[:image].split('/').last }
155
+
156
+ begin
157
+ if existing_image.present?
158
+ existing_image.update_attributes!(
159
+ image_attributes
160
+ )
161
+ return
162
+ else
163
+ image = product.images.build(image_attributes)
164
+ image.save!
165
+ end
166
+ rescue
167
+ puts "Error creating image #{attrs[:image]}"
168
+ end
169
+ end
170
+
171
+ # Creates a redirect based on the attributes url_path
172
+ #
173
+ # @param attrs [Hash] a hash of magento product data
174
+ # @return [Workarea::Navigation::Redirect]
175
+ def create_redirect(attrs)
176
+ return unless attrs[:url_path].present?
177
+ path = attrs[:url_path]
178
+ destination = ProductUrl.product_path(product)
179
+
180
+ Workarea::Navigation::Redirect.create(path: path, destination: destination)
181
+ end
182
+
183
+ def inventory_policy(attrs)
184
+ if attrs[:backorders] == 1
185
+ "allow_backorder"
186
+ else
187
+ "standard"
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,110 @@
1
+ module Workarea
2
+ module MagentoDataImporter
3
+ class ConfigurableProduct < BaseProduct
4
+ attr_reader :magento_product, :product_data
5
+
6
+ def process
7
+ product.assign_attributes(product_attributes)
8
+ product.filters = category_filters(product_data)
9
+
10
+ product_details = build_product_details(product_data)
11
+ product.update_details(product_details)
12
+
13
+ product.save! rescue puts "Error saving #{magento_product.magento_product_id}"
14
+
15
+ create_variants
16
+
17
+ create_categories(product_data)
18
+
19
+ create_redirect(product_data)
20
+
21
+ magento_product.update_attributes!(imported: true)
22
+ end
23
+
24
+ private
25
+
26
+ def variant_details_row(products)
27
+ products.detect { |p| p.product_data[:price].present? }
28
+ end
29
+
30
+ def variant_products
31
+ @variant_products ||= Workarea::Import::MagentoProduct.where(magento_product_id: magento_product.magento_product_id)
32
+ end
33
+
34
+ def create_variants
35
+ variant_products.each_with_index do |variant_product, variant_position = 1|
36
+ variant_product_data = variant_product.product_data.deep_symbolize_keys
37
+
38
+ next unless variant_product_data[:_super_products_sku].present?
39
+
40
+ sku = variant_product_data[:_super_products_sku].parameterize
41
+
42
+ # the "super_products_sku" field points to a record with the required details
43
+ detail_products = associated_products(variant_product_data[:_super_products_sku])
44
+
45
+ # get the record with the pricing and inventory
46
+ details_row = variant_details_row(detail_products)
47
+
48
+ # create the variant sku
49
+ variant = product.variants.find_or_initialize_by(sku: sku)
50
+ variant.name = variant_product_data[:sku]
51
+
52
+ options = row_filters(variant_product_data)
53
+ variant.update_details(options)
54
+
55
+ variant.save! rescue (puts "#{sku} variant could not be saved" && next)
56
+
57
+ pricing_sku = Workarea::Pricing::Sku.find_or_initialize_by(id: sku)
58
+
59
+ msrp = details_row.product_data[:msrp].to_m
60
+ pricing_sku.msrp = msrp if msrp > 0.to_m
61
+ pricing_sku.prices = [{ regular: details_row.product_data[:price].to_m }]
62
+ pricing_sku.save! rescue (puts "pricing #{sku} variant could not be saved" && next)
63
+
64
+ # create the inventory sku
65
+ inventory_sku = Workarea::Inventory::Sku.find_or_initialize_by(id: sku)
66
+ inventory_sku.available = details_row.product_data[:qty] || 0
67
+ inventory_sku.policy = inventory_policy(details_row.product_data)
68
+ inventory_sku.save! rescue (puts "inventory #{sku} variant could not be saved" && next)
69
+
70
+ detail_product_filters = product.filters
71
+
72
+ detail_products.each do |detail_product|
73
+ detail_product_data = detail_product.product_data.deep_symbolize_keys
74
+ build_image(detail_product_data)
75
+
76
+ create_categories(detail_product_data)
77
+
78
+ detail_product_category_filters = category_filters(detail_product_data)
79
+ detail_product_filters = add_filter_values(detail_product_filters, detail_product_category_filters)
80
+
81
+ new_filters = row_filters(detail_product_data)
82
+ detail_product_filters = add_filter_values(detail_product_filters, new_filters)
83
+
84
+ detail_product.imported = true
85
+ detail_product.save!
86
+
87
+ create_redirect(detail_product_data)
88
+ end
89
+
90
+ # Create the categories for variants that are categorized differently than their
91
+ # parent products.
92
+ create_categories(variant_product_data)
93
+ variant_product_category_filters = category_filters(variant_product_data)
94
+ detail_product_filters = add_filter_values(detail_product_filters, variant_product_category_filters)
95
+
96
+ product.filters = detail_product_filters
97
+ product.save!
98
+
99
+ create_redirect(variant_product_data)
100
+
101
+ variant_product.update_attributes!(imported: true)
102
+ end
103
+ end
104
+
105
+ def associated_products(sku)
106
+ Workarea::Import::MagentoProduct.where(magento_product_id: sku)
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,10 @@
1
+ require 'workarea/magento_data_importer'
2
+
3
+ module Workarea
4
+ module MagentoDataImporter
5
+ class Engine < ::Rails::Engine
6
+ include Workarea::Plugin
7
+ isolate_namespace Workarea::MagentoDataImporter
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,86 @@
1
+ module Workarea
2
+ module MagentoDataImporter
3
+ class ImportProducts
4
+ def self.import!(file_path)
5
+ # purge existing data
6
+ Workarea::Import::MagentoProduct.delete_all
7
+
8
+ current_id = nil
9
+
10
+ CSV.foreach(file_path, csv_options) do |row|
11
+ current_id = row[:sku] if row[:sku].present?
12
+
13
+ Workarea::Import::MagentoProduct.create!(
14
+ product_data: row.to_hash,
15
+ magento_product_id: current_id,
16
+ product_type: row[:_type]
17
+ )
18
+ end
19
+
20
+ # get distinct values for creating the taxonomy, get this list before import because the
21
+ # import collection is deleted after import.
22
+ import_categories = Workarea::Import::MagentoProduct.distinct("product_data._category").compact!.sort!
23
+ Sidekiq::Callbacks.disable do
24
+ puts "Creating Configurable Products"
25
+ process_configurable_products
26
+
27
+ puts "Creating Simple Products"
28
+ process_simple_products
29
+
30
+ puts "Creating Taxonomy"
31
+ process_taxonomy(import_categories)
32
+
33
+ puts "Creating Navigation Menu"
34
+ process_navigation_menu
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def self.process_configurable_products
41
+ Workarea::Import::MagentoProduct.parent_products.each do |parent_product|
42
+ begin
43
+ MagentoDataImporter::ConfigurableProduct.new(parent_product).process
44
+ rescue
45
+ puts "Error importing product #{parent_product.magento_product_id}"
46
+ parent_product.update_attributes!(import_failed: true)
47
+ end
48
+ end
49
+ Workarea::Import::MagentoProduct.where(imported: true).delete_all
50
+ end
51
+
52
+ def self.process_simple_products
53
+ Workarea::Import::MagentoProduct.simple_products.each do |simple_product|
54
+ begin
55
+ MagentoDataImporter::SimpleProduct.new(simple_product).process
56
+ rescue
57
+ puts "Error importing product #{simple_product.magento_product_id}"
58
+ simple_product.update_attributes!(import_failed: true)
59
+ end
60
+ end
61
+ Workarea::Import::MagentoProduct.where(imported: true).delete_all
62
+ end
63
+
64
+ def self.process_taxonomy(import_categories)
65
+ import_categories.each do |import_category|
66
+ Workarea::MagentoDataImporter::Taxonomy.new(import_category).process
67
+ end
68
+ end
69
+
70
+ def self.process_navigation_menu
71
+ taxons = Workarea::Navigation::Taxon.where(depth: 1)
72
+ taxons.each do |taxon|
73
+ Workarea::Navigation::Menu.create!(taxon: taxon)
74
+ end
75
+ end
76
+
77
+ def self.csv_options
78
+ {
79
+ headers: true,
80
+ return_headers: false,
81
+ header_converters: -> (h) { h.underscore.optionize.to_sym }
82
+ }
83
+ end
84
+ end
85
+ end
86
+ end