magento 0.13.1 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb31fed59d9d16923e61b3f77d218087e2c2dc0e40912f59c0f0f447886ec6a4
4
- data.tar.gz: 7d93a4d2dcef4ad968a7dacd9eb0445f7f6885791ef202873df555efad9fc4dd
3
+ metadata.gz: 73c1f0ddb533c84f3cb909d31a6349fc51237965c2e60ff92f23bf30da582454
4
+ data.tar.gz: f269b8b2a89e8c7726782cbc2638cff88ab020495807baa5ed39f874283ea657
5
5
  SHA512:
6
- metadata.gz: 0b15c59d8eb5f07ebc3df8e875128463790c3a26f6e96f0b076404a3aa18c7760f0d66636f3292b596f0d527da180f1125a8789dc59ed38b769b215bf02be580
7
- data.tar.gz: 193242af27b216087e158b95bd136ee9fc70b66ca0e68711e5eda257b854beba663949a70a385b495191b12273ded59fb7dec44595a28d52704a4fe5ea1f38d5
6
+ metadata.gz: d6efe97f07e753ef46abe67223d4503c605fbc4d060a0dfc0433f06134fa5b2e59088f7664b7f5e711598f46e801666b70912fcd9fdf38ea1760ef866b9edf76
7
+ data.tar.gz: 3c3b2c9b3f3d018665e9b0d2b47102a880194446bc8bb4423e002ead7ee345e42beda903f20843862987ad28751a5743a18ae77d100724c95d7884e772e8b9df
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  Add in your Gemfile
6
6
 
7
7
  ```rb
8
- gem 'magento', '~> 0.13.1'
8
+ gem 'magento', '~> 0.15.1'
9
9
  ```
10
10
 
11
11
  or run
@@ -339,6 +339,28 @@ cart.payment_information(
339
339
  >> "234575" # return the order id
340
340
  ```
341
341
 
342
+ Add coupon to cart
343
+ ```rb
344
+ cart = Magento::GuestCart.find('gXsepZcgJbY8RCJXgGioKOO9iBCR20r7')
345
+
346
+ cart.add_coupon('COAU4HXE0I')
347
+ # You can also use the class method
348
+ Magento::GuestCart.add_coupon('gXsepZcgJbY8RCJXgGioKOO9iBCR20r7', 'COAU4HXE0I')
349
+
350
+ >> true # return true on success
351
+ ```
352
+
353
+ Delete coupon from cart
354
+ ```rb
355
+ cart = Magento::GuestCart.find('gXsepZcgJbY8RCJXgGioKOO9iBCR20r7')
356
+
357
+ cart.delete_coupon()
358
+ # You can also use the class method
359
+ Magento::GuestCart.delete_coupon('gXsepZcgJbY8RCJXgGioKOO9iBCR20r7')
360
+
361
+ >> true # return true on success
362
+ ```
363
+
342
364
  ## Invoice an Order
343
365
 
344
366
  ```rb
@@ -540,7 +562,7 @@ rule = Magento::SalesRule.create(
540
562
  rule.generate_coupon(quantity: 1, length: 10)
541
563
  ```
542
564
 
543
- Renarate by class method
565
+ Generate by class method
544
566
  ```rb
545
567
  Magento::SalesRule.generate_coupon(
546
568
  couponSpec: {
@@ -553,6 +575,8 @@ Magento::SalesRule.generate_coupon(
553
575
  see all params in:
554
576
  - [Magento docs Coupon](https://magento.redoc.ly/2.3.5-admin/tag/couponsgenerate#operation/salesRuleCouponManagementV1GeneratePost)
555
577
  - [Magento docs SalesRules](https://magento.redoc.ly/2.3.5-admin/tag/salesRules#operation/salesRuleRuleRepositoryV1SavePost)
578
+
579
+ See [how to add coupons to cart](#guestcart)
556
580
 
557
581
  ### First result
558
582
  ```rb
@@ -5,6 +5,7 @@ require 'dry/inflector'
5
5
  require 'active_support/core_ext/string/inflections'
6
6
  require 'active_support/core_ext/hash/keys'
7
7
 
8
+ require_relative 'magento/configuration'
8
9
  require_relative 'magento/errors'
9
10
  require_relative 'magento/request'
10
11
  require_relative 'magento/model_mapper'
@@ -21,13 +22,16 @@ require_relative 'magento/order'
21
22
  require_relative 'magento/invoice'
22
23
  require_relative 'magento/guest_cart'
23
24
  require_relative 'magento/sales_rule'
25
+ require_relative 'magento/import'
24
26
 
25
27
  Dir[File.expand_path('magento/shared/*.rb', __dir__)].map { |f| require f }
26
28
  Dir[File.expand_path('magento/params/*.rb', __dir__)].map { |f| require f }
27
29
 
28
30
  module Magento
29
31
  class << self
30
- attr_accessor :url, :open_timeout, :timeout, :token, :store
32
+ attr_writer :configuration
33
+
34
+ delegate :url=, :token=, :store=, :open_timeout=, :timeout=, to: :configuration
31
35
 
32
36
  def inflector
33
37
  @inflector ||= Dry::Inflector.new do |inflections|
@@ -36,26 +40,24 @@ module Magento
36
40
  end
37
41
  end
38
42
 
39
- self.url = ENV['MAGENTO_URL']
40
- self.open_timeout = 30
41
- self.timeout = 90
42
- self.token = ENV['MAGENTO_TOKEN']
43
- self.store = ENV['MAGENTO_STORE'] || :all
43
+ def self.configuration
44
+ @configuration ||= Configuration.new
45
+ end
44
46
 
45
- def self.with_config(utl: Magento.url, token: Magento.token, store: Magento.store)
46
- @old_url = self.url
47
- @old_token = self.token
48
- @old_store = self.store
49
-
50
- self.url = utl
51
- self.token = token
52
- self.store = store
47
+ def self.reset
48
+ @configuration = Configuration.new
49
+ end
50
+
51
+ def self.configure
52
+ yield(configuration)
53
+ end
53
54
 
55
+ def self.with_config(params)
56
+ @old_configuration = configuration
57
+ self.configuration = configuration.copy_with(**params)
54
58
  yield
55
59
  ensure
56
- self.url = @old_url
57
- self.token = @old_token
58
- self.store = @old_store
60
+ @configuration = @old_configuration
59
61
  end
60
62
 
61
63
  def self.production?
@@ -0,0 +1,31 @@
1
+ module Magento
2
+ class Configuration
3
+ attr_accessor :url, :open_timeout, :timeout, :token, :store, :product_image
4
+
5
+ def initialize(url: nil, token: nil, store: nil)
6
+ self.url = url || ENV['MAGENTO_URL']
7
+ self.open_timeout = 30
8
+ self.timeout = 90
9
+ self.token = token || ENV['MAGENTO_TOKEN']
10
+ self.store = store || ENV['MAGENTO_STORE'] || :all
11
+
12
+ self.product_image = ProductImageConfiguration.new
13
+ end
14
+
15
+ def copy_with(params = {})
16
+ clone.tap do |config|
17
+ params.each { |key, value| config.send("#{key}=", value) }
18
+ end
19
+ end
20
+ end
21
+
22
+ class ProductImageConfiguration
23
+ attr_accessor :small_size, :medium_size, :large_size
24
+
25
+ def initialize
26
+ self.small_size = '200x200>'
27
+ self.medium_size = '400x400>'
28
+ self.large_size = '800x800>'
29
+ end
30
+ end
31
+ end
@@ -33,7 +33,7 @@ module Magento
33
33
  end
34
34
 
35
35
  def find_by_token(token)
36
- user_request = Request.new(token: token)
36
+ user_request = Request.new(config: Magento.configuration.copy_with(token: token))
37
37
  customer_hash = user_request.get('customers/me').parse
38
38
  build(customer_hash)
39
39
  end
@@ -32,6 +32,29 @@ module Magento
32
32
  self.class.payment_information(attributes)
33
33
  end
34
34
 
35
+ #
36
+ # Add a coupon by code to the current cart.
37
+ #
38
+ # Example
39
+ # cart = Magento::GuestCart.find('gXsepZcgJbY8RCJXgGioKOO9iBCR20r7')
40
+ # cart.add_coupon('COAU4HXE0I')
41
+ #
42
+ # @return Boolean: true on success, false otherwise
43
+ def add_coupon(coupon)
44
+ self.class.add_coupon(cart_id, coupon)
45
+ end
46
+
47
+ # Delete cart's coupon
48
+ #
49
+ # Example:
50
+ # cart = Magento::GuestCart.find('gXsepZcgJbY8RCJXgGioKOO9iBCR20r7')
51
+ # cart.delete_coupon()
52
+ #
53
+ # @return Boolean: true on success, raise exception otherwise
54
+ def delete_coupon
55
+ self.class.delete_coupon(cart_id)
56
+ end
57
+
35
58
  class << self
36
59
  def create(load_cart_info: false)
37
60
  cart = build(cart_id: request.post(api_resource).parse)
@@ -64,6 +87,33 @@ module Magento
64
87
  hash = request.post(url, attributes).parse
65
88
  Magento::ModelMapper.map_hash(Magento::Item, hash)
66
89
  end
90
+
91
+ #
92
+ # Add a coupon by code to a specified cart.
93
+ #
94
+ # Example
95
+ # Magento::GuestCart.add_coupon(
96
+ # 'aj8oUtY1Qi44Fror6UWVN7ftX1idbBKN',
97
+ # 'COAU4HXE0I'
98
+ # )
99
+ #
100
+ # @return Boolean: true on success, false otherwise
101
+ def add_coupon(id, coupon)
102
+ url = "#{api_resource}/#{id}/coupons/#{coupon}"
103
+ request.put(url, nil).parse
104
+ end
105
+
106
+ #
107
+ # Delete a coupon from a specified cart.
108
+ #
109
+ # Example:
110
+ # Magento::GuestCart.delete_coupon('aj8oUtY1Qi44Fror6UWVN7ftX1idbBKN')
111
+ #
112
+ # @return Boolean: true on success, raise exception otherwise
113
+ def delete_coupon(id)
114
+ url = "#{api_resource}/#{id}/coupons"
115
+ request.delete(url).parse
116
+ end
67
117
  end
68
118
  end
69
119
  end
@@ -0,0 +1,18 @@
1
+ require_relative 'import/image_finder'
2
+ require_relative 'import/csv_reader'
3
+ require_relative 'import/category'
4
+ require_relative 'import/product'
5
+
6
+ module Magento
7
+ module Import
8
+ def self.from_csv(file, images_folder: nil, website_ids: [0])
9
+ products = CSVReader.new(file).get_products
10
+ products = Category.new(products).associate
11
+ Product.new(website_ids, images_folder).import(products)
12
+ end
13
+
14
+ def self.get_csv_template
15
+ File.open(__dir__ + '/import/template/products.csv')
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ module Magento
2
+ module Import
3
+ class ImageFinder
4
+ EXTENTIONS = %w[jpg jpeg png webp gif].freeze
5
+
6
+ def initialize(images_folder)
7
+ @images_folder = images_folder
8
+ end
9
+
10
+ def find_by_name(name)
11
+ prefix = "#{@images_folder}/#{name}"
12
+
13
+ EXTENTIONS.map { |e| ["#{prefix}.#{e}", "#{prefix}.#{e.upcase}"] }.flatten
14
+ .find { |file| File.exist?(file) }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,51 @@
1
+ module Magento
2
+ module Import
3
+ class Category
4
+ def initialize(products)
5
+ @products = products
6
+ @category_root = Magento::Category.all
7
+ @cats = @category_root.children_data
8
+ end
9
+
10
+ def associate
11
+ @products.each do |prod|
12
+ cat1 = find_or_create(name: prod.cat1, parent: @category_root) if prod.cat1
13
+ cat2 = find_or_create(name: prod.cat2, parent: cat1) if prod.cat2
14
+ cat3 = find_or_create(name: prod.cat3, parent: cat2) if prod.cat3
15
+
16
+ prod.cat1, prod.cat2, prod.cat3 = cat1&.id, cat2&.id, cat3&.id
17
+
18
+ @cats.push(*[cat1, cat2, cat3].compact)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def find_or_create(name:, parent:)
25
+ find(name, cats: @cats, parent_id: parent.id) || create(name, parent: parent)
26
+ end
27
+
28
+ def find(name, cats:, parent_id:)
29
+ cats.each do |cat|
30
+ return cat if cat.name == name && cat.parent_id == parent_id
31
+
32
+ if cat.respond_to?(:children_data) && cat.children_data&.size.to_i > 0
33
+ result = find(name, cats: cat.children_data, parent_id: parent_id)
34
+ return result if result
35
+ end
36
+ end
37
+ nil
38
+ end
39
+
40
+ def create(name, parent:)
41
+ params = Magento::Params::CreateCategoria.new(
42
+ name: name,
43
+ parent_id: parent.id,
44
+ url: "#{Magento.configuration.store}-#{parent.id}-#{name.parameterize}"
45
+ )
46
+
47
+ Magento::Category.create(params.to_h).tap { |c| puts "Create: #{c.id} => #{c.name}" }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,30 @@
1
+ require 'csv'
2
+ require 'ostruct'
3
+
4
+ module Magento
5
+ module Import
6
+ class CSVReader
7
+ def initialize(csv_file)
8
+ @csv = CSV.read(csv_file, col_sep: ';')
9
+ end
10
+
11
+ def get_products
12
+ @csv[1..].map do |row|
13
+ OpenStruct.new({
14
+ name: row[0],
15
+ sku: row[1],
16
+ ean: row[2],
17
+ description: row[3],
18
+ price: row[4],
19
+ special_price: row[5],
20
+ quantity: row[6],
21
+ cat1: row[7],
22
+ cat2: row[8],
23
+ cat3: row[9],
24
+ main_image: row[10]
25
+ })
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,59 @@
1
+ require 'uri'
2
+
3
+ module Magento
4
+ module Import
5
+ class Product
6
+ def initialize(website_ids, images_folder = nil)
7
+ @website_ids = website_ids
8
+ @image_finder = images_folder ? ImageFinder.new(images_folder) : nil
9
+ end
10
+
11
+ def import(products)
12
+ products.each do |product|
13
+ params = Magento::Params::CreateProduct.new(
14
+ sku: product.sku,
15
+ name: product.name.gsub(/[ ]+/, ' '),
16
+ description: product.description || product.name,
17
+ brand: product.brand,
18
+ price: product.price.to_f,
19
+ special_price: product.special_price ? product.special_price.to_f : nil,
20
+ quantity: numeric?(product.quantity) ? product.quantity.to_f : 0,
21
+ weight: 0.3,
22
+ manage_stock: numeric?(product.quantity),
23
+ attribute_set_id: 4,
24
+ category_ids: [product.cat1, product.cat2, product.cat3].compact,
25
+ website_ids: @website_ids,
26
+ images: images(product)
27
+ ).to_h
28
+
29
+ product = Magento::Product.create(params)
30
+
31
+ puts "Produto criado: #{product.sku} => #{product.name}"
32
+ rescue => e
33
+ puts "Erro ao criado: #{product.sku} => #{product.name}"
34
+ puts " - Detalhes do erro: #{e}"
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def images(product)
41
+ return [] unless product.main_image.to_s =~ URI::regexp || @image_finder
42
+
43
+ image = product.main_image || @image_finder.find_by_name(product.sku)
44
+ return [] unless image
45
+
46
+ Magento::Params::CreateImage.new(
47
+ path: image,
48
+ title: product.name,
49
+ position: 0,
50
+ main: true
51
+ ).variants
52
+ end
53
+
54
+ def numeric?(value)
55
+ !!(value.to_s =~ /^[\d]+$/)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,2 @@
1
+ name;sku;gtin;description;price;special price;quantity;category level 1;category level 2;category level 3;main image url
2
+ Apple iPhone 11 Pro;iphone-11-pro;9999999999;"Description: New Apple iPhone 11 Pro Max 64/256/512GB Space Gray Midnight Green Silver Gold\nIncludes all new accessories\n\niPhone is unlocked to work on any GSM carrier!\n\nNo warranty - Warranty can be purchased through squaretrade\n\nShipping: We Normally Ship Out Same or Next Business Day Via USPS. \nDelivery Time Varies Especially During Holidays.\nWe Do NOT Ship Out On Saturday/Sunday\nWe Are Not Responsible For Late Packages, But We Will Find Out If There Are Any Issues.\nWe Only Ship To PayPal Confirmed Address.\nPick Up Also Available In Queens NY (Message For Details)\nPriority Mail USPS Free (1-4 Business Days; Not Guaranteed Service)\nExpress Overnight USPS $25 (Guaranteed By USPS Overnight To Most Places) \n\nAdditional return policy details: \nAs a leading seller of electronics, we want to guarantee that each transaction ends with a five star experience. That's why we offer a FREE no questions asked 30-day return policy. Please message us for more details.\n\nSales Tax:\nNew York orders will have a sales tax of 8.875% add to your cost";1099.99;999.00;45;Technology;Smartphone;Apple;"https://store.storeimages.cdn-apple.com/4982/as-images.apple.com/is/iphone-11-pro-select-2019-family?wid=882&amp;hei=1058&amp;fmt=jpeg&amp;qlt=80&amp;op_usm=0.5,0.5&amp;.v=1586586488946"
@@ -4,17 +4,17 @@ module Magento
4
4
  module Params
5
5
  class CreateCategoria < Dry::Struct
6
6
  attribute :name, Type::String
7
- attribute :parent_id, Type::String.optional
8
- attribute :path, Type::String.optional
7
+ attribute :parent_id, Type::Integer.optional
8
+ attribute :url, Type::String.optional.default(nil)
9
9
  attribute :is_active, Type::Bool.default(true)
10
10
 
11
11
  def to_h
12
12
  {
13
- "name": name,
14
- "parent_id": parent_id,
15
- "is_active": is_active,
16
- "path": path
17
- }
13
+ name: name,
14
+ parent_id: parent_id,
15
+ is_active: is_active,
16
+ custom_attributes: url ? [{attribute_code: 'url_key', value: url }] : nil
17
+ }.compact
18
18
  end
19
19
  end
20
20
  end
@@ -7,9 +7,9 @@ module Magento
7
7
  module Params
8
8
  class CreateImage < Dry::Struct
9
9
  VARIANTS = {
10
- 'large' => { size: '800x800', type: :image },
11
- 'medium' => { size: '300x300', type: :small_image },
12
- 'small' => { size: '100x100', type: :thumbnail }
10
+ 'large' => :image,
11
+ 'medium' => :small_image,
12
+ 'small' => :thumbnail
13
13
  }.freeze
14
14
 
15
15
  attribute :title, Type::String
@@ -30,7 +30,7 @@ module Magento
30
30
  "type": mini_type,
31
31
  "name": filename
32
32
  },
33
- "types": main ? [VARIANTS[size][:type]] : []
33
+ "types": main ? [VARIANTS[size]] : []
34
34
  }
35
35
  end
36
36
 
@@ -48,9 +48,19 @@ module Magento
48
48
 
49
49
  def file
50
50
  @file ||= MiniMagick::Image.open(path).tap do |b|
51
- b.resize VARIANTS[size][:size]
52
- b.strip
51
+ b.resize(Magento.configuration.product_image.send(size + '_size'))
52
+ bigger_side = b.dimensions.max
53
+ b.combine_options do |c|
54
+ c.background '#FFFFFF'
55
+ c.alpha 'remove'
56
+ c.gravity 'center'
57
+ c.extent "#{bigger_side}x#{bigger_side}"
58
+ c.strip
59
+ end
60
+ b.format 'jpg'
53
61
  end
62
+ rescue => e
63
+ raise "Erro ao ler imagem #{path}: #{e}"
54
64
  end
55
65
 
56
66
  def filename
@@ -54,7 +54,7 @@ module Magento
54
54
  attribute :sku, Type::String
55
55
  attribute :name, Type::String
56
56
  attribute :description, Type::String
57
- attribute :brand, Type::String
57
+ attribute :brand, Type::String.optional.default(nil)
58
58
  attribute :price, Type::Coercible::Float
59
59
  attribute :special_price, Type::Float.optional.default(nil)
60
60
  attribute :attribute_set_id, Type::Integer
@@ -128,10 +128,11 @@ module Magento
128
128
  default_attributes = [
129
129
  CustomAttribute.new(attribute_code: 'description', value: description),
130
130
  CustomAttribute.new(attribute_code: 'url_key', value: name.parameterize ),
131
- CustomAttribute.new(attribute_code: 'product_brand', value: brand ),
132
131
  CustomAttribute.new(attribute_code: 'featured', value: featured)
133
132
  ]
134
133
 
134
+ default_attributes.push(CustomAttribute.new(attribute_code: 'product_brand', value: brand)) if brand
135
+
135
136
  if special_price.to_f > 0
136
137
  default_attributes << CustomAttribute.new(attribute_code: 'special_price', value: special_price.to_s)
137
138
  end
@@ -85,8 +85,11 @@ module Magento
85
85
 
86
86
  def all
87
87
  result = request.get("#{endpoint}?#{query_params}").parse
88
- field = model == Magento::Category ? 'children_data' : 'items'
89
- RecordCollection.from_magento_response(result, model: model, iterable_field: field)
88
+ if model == Magento::Category
89
+ model.build(result)
90
+ else
91
+ RecordCollection.from_magento_response(result, model: model)
92
+ end
90
93
  end
91
94
 
92
95
  def first
@@ -5,11 +5,10 @@ require 'http'
5
5
 
6
6
  module Magento
7
7
  class Request
8
- attr_reader :token, :store
8
+ attr_reader :config
9
9
 
10
- def initialize(token: Magento.token, store: Magento.store)
11
- @token = token
12
- @store = store
10
+ def initialize(config: Magento.configuration)
11
+ @config = config
13
12
  end
14
13
 
15
14
  def get(resource)
@@ -36,12 +35,13 @@ module Magento
36
35
  private
37
36
 
38
37
  def http_auth
39
- HTTP.auth("Bearer #{token}")
38
+ HTTP.auth("Bearer #{config.token}")
39
+ .timeout(connect: config.timeout, read: config.open_timeout)
40
40
  end
41
41
 
42
42
  def base_url
43
- url = Magento.url.to_s.sub(%r{/$}, '')
44
- "#{url}/rest/#{store}/V1"
43
+ url = config.url.to_s.sub(%r{/$}, '')
44
+ "#{url}/rest/#{config.store}/V1"
45
45
  end
46
46
 
47
47
  def url(resource)
@@ -53,10 +53,8 @@ module Magento
53
53
 
54
54
  begin
55
55
  msg = resp.parse['message']
56
- errors = resp.parse['errors']
57
- if resp.parse['parameters'].is_a? Hash
58
- resp.parse['parameters'].each { |k, v| msg.sub! "%#{k}", v }
59
- end
56
+ errors = resp.parse['errors'] || resp.parse['parameters']
57
+ resp.parse['parameters'].each { |k, v| msg.sub! "%#{k}", v } if resp.parse['parameters'].is_a? Hash
60
58
  rescue StandardError
61
59
  msg = 'Failed access to the magento server'
62
60
  errors = []
@@ -1,3 +1,3 @@
1
1
  module Magento
2
- VERSION = '0.13.1'
2
+ VERSION = '0.17.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: magento
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.1
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wallas Faria
@@ -92,10 +92,17 @@ files:
92
92
  - README.md
93
93
  - lib/magento.rb
94
94
  - lib/magento/category.rb
95
+ - lib/magento/configuration.rb
95
96
  - lib/magento/country.rb
96
97
  - lib/magento/customer.rb
97
98
  - lib/magento/errors.rb
98
99
  - lib/magento/guest_cart.rb
100
+ - lib/magento/import.rb
101
+ - lib/magento/import/Image_finder.rb
102
+ - lib/magento/import/category.rb
103
+ - lib/magento/import/csv_reader.rb
104
+ - lib/magento/import/product.rb
105
+ - lib/magento/import/template/products.csv
99
106
  - lib/magento/invoice.rb
100
107
  - lib/magento/model.rb
101
108
  - lib/magento/model_mapper.rb