Product Import v3 with asynchronous processing

Fixed spec

Quick spec tweak
This commit is contained in:
Matt-Yorkley
2017-04-21 20:11:12 +01:00
parent 3a650dd8b3
commit b3c906b3a4
22 changed files with 1124 additions and 474 deletions

View File

@@ -1,5 +1,4 @@
angular.module("ofn.admin").controller "ImportFeedbackCtrl", ($scope, productImportData) ->
$scope.entries = productImportData
angular.module("ofn.admin").controller "ImportFeedbackCtrl", ($scope) ->
$scope.count = (items) ->
total = 0
@@ -8,4 +7,4 @@ angular.module("ofn.admin").controller "ImportFeedbackCtrl", ($scope, productImp
total
$scope.attribute_invalid = (attribute, line_number) ->
$scope.entries[line_number]['errors'][attribute] != undefined
$scope.entries[line_number]['errors'][attribute] != undefined

View File

@@ -0,0 +1,159 @@
angular.module("ofn.admin").controller "ImportFormCtrl", ($scope, $http, $filter, ProductImportService, $timeout) ->
$scope.entries = {}
$scope.update_counts = {}
$scope.reset_counts = {}
#$scope.import_options = {}
$scope.updates = {}
$scope.updated_total = 0
$scope.updated_ids = []
$scope.update_errors = []
$scope.chunks = 0
$scope.completed = 0
$scope.percentage = "0%"
$scope.started = false
$scope.finished = false
$scope.countResettable = () ->
angular.forEach $scope.supplier_product_counts, (value, key) ->
$scope.reset_counts[key] = value
if $scope.update_counts[key]
$scope.reset_counts[key] -= $scope.update_counts[key]
$scope.resetProgress = () ->
$scope.chunks = 0
$scope.completed = 0
$scope.percentage = "0%"
$scope.started = false
$scope.finished = false
$scope.step = 'import'
$scope.viewResults = () ->
$scope.countResettable()
$scope.step = 'results'
$scope.resetProgress()
$scope.acceptResults = () ->
$scope.step = 'save'
$scope.finalResults = () ->
$scope.step = 'complete'
$scope.start = () ->
$scope.started = true
$scope.percentage = "1%"
total = $scope.item_count
size = 100
$scope.chunks = Math.ceil(total / size)
i = 0
while i < $scope.chunks
start = (i*size)+1
end = (i+1)*size
if $scope.step == 'import'
$scope.processImport(start, end)
if $scope.step == 'save'
$scope.processSave(start, end)
i++
$scope.processImport = (start, end) ->
$http(
url: $scope.import_url
method: 'POST'
data:
'start': start
'end': end
'filepath': $scope.filepath
'import_into': $scope.import_into
).success((data, status, headers, config) ->
angular.merge($scope.entries, angular.fromJson(data['entries']))
$scope.sortUpdates(data['reset_counts'])
$scope.updateProgress()
).error((data, status, headers, config) ->
console.log('Error: '+status)
)
$scope.importSettings = null
$scope.getSettings = () ->
$scope.importSettings = ProductImportService.getSettings()
$scope.sortUpdates = (data) ->
angular.forEach data, (value, key) ->
if (key in $scope.update_counts)
$scope.update_counts[key] += value['updates_count']
else
$scope.update_counts[key] = value['updates_count']
$scope.processSave = (start, end) ->
$scope.getSettings() if $scope.importSettings == null
$http(
url: $scope.save_url
method: 'POST'
data:
'start': start
'end': end
'filepath': $scope.filepath
'import_into': $scope.import_into,
'settings': $scope.importSettings
).success((data, status, headers, config) ->
$scope.sortResults(data['results'])
angular.forEach data['updated_ids'], (id) ->
$scope.updated_ids.push(id)
angular.forEach data['errors'], (error) ->
$scope.update_errors.push(error)
$scope.updateProgress()
).error((data, status, headers, config) ->
console.log('Error: '+status)
)
$scope.sortResults = (results) ->
angular.forEach results, (value, key) ->
if ($scope.updates[key] != undefined)
$scope.updates[key] += value
else
$scope.updates[key] = value
$scope.updated_total += value
$scope.resetAbsent = () ->
enterprises_to_reset = []
angular.forEach $scope.importSettings, (settings, enterprise) ->
if settings['reset_all_absent']
enterprises_to_reset.push(enterprise)
if enterprises_to_reset.length && $scope.updated_ids.length
$http(
url: $scope.reset_url
method: 'POST'
data:
'filepath': $scope.filepath
'import_into': $scope.import_into,
'settings': $scope.importSettings
'reset_absent': true,
'updated_ids': $scope.updated_ids,
'enterprises_to_reset': enterprises_to_reset
).success((data, status, headers, config) ->
console.log(data)
$scope.updates.products_reset = data
).error((data, status, headers, config) ->
console.log('Error: '+status)
)
$scope.updateProgress = () ->
$scope.completed++
$scope.percentage = String(Math.round(($scope.completed / $scope.chunks) * 100)) + '%'
if $scope.completed == $scope.chunks
$scope.finished = true
$scope.resetAbsent() if $scope.step == 'save'

View File

@@ -1,12 +1,31 @@
angular.module("ofn.admin").controller "ImportOptionsFormCtrl", ($scope, $rootScope, ProductImportService) ->
$scope.toggleResetAbsent = () ->
confirmed = confirm t('js.product_import.confirmation') if $scope.resetAbsent
$scope.initForm = () ->
$scope.settings = {} if $scope.settings == undefined
$scope.settings[$scope.supplierId] = {
defaults:
count_on_hand:
mode: 'overwrite_all'
on_hand:
mode: 'overwrite_all'
tax_category_id:
mode: 'overwrite_all'
shipping_category_id:
mode: 'overwrite_all'
available_on:
mode: 'overwrite_all'
}
if confirmed or !$scope.resetAbsent
ProductImportService.updateResetAbsent($scope.supplierId, $scope.resetCount, $scope.resetAbsent)
else
$scope.resetAbsent = false
$scope.$watch 'settings', (updated) ->
ProductImportService.updateSettings(updated)
, true
$scope.toggleResetAbsent = (id) ->
resetAbsent = $scope.settings[id]['reset_all_absent']
confirmed = confirm t('js.product_import.confirmation') if resetAbsent
if confirmed or !resetAbsent
ProductImportService.updateResetAbsent($scope.supplierId, $scope.reset_counts[$scope.supplierId], resetAbsent)
$scope.resetTotal = ProductImportService.resetTotal

View File

@@ -2,6 +2,7 @@ angular.module("ofn.admin").factory "ProductImportService", ($rootScope) ->
new class ProductImportService
suppliers: {}
resetTotal: 0
settings: {}
updateResetAbsent: (supplierId, resetCount, resetAbsent) ->
if resetAbsent
@@ -13,3 +14,8 @@ angular.module("ofn.admin").factory "ProductImportService", ($rootScope) ->
$rootScope.resetTotal = @resetTotal
updateSettings: (updated) ->
angular.merge(@settings, updated)
getSettings: () ->
@settings

View File

@@ -238,7 +238,7 @@ table.import-settings {
}
}
form.product-import, div.post-save-results {
form.product-import, div.post-save-results, div.import-wrapper {
input[type="submit"] {
margin-right: 0.5em;
}
@@ -246,4 +246,59 @@ form.product-import, div.post-save-results {
min-width: 8em;
text-align: center;
}
}
}
form.product-import, div.save-results {
transition: all linear 0.25s;
}
form.product-import.ng-hide, div.save-results.ng-hide {
opacity: 0;
}
div.import-wrapper {
div.progress-interface {
text-align: center;
transition: all linear 0.25s;
button {
}
button:disabled {
background: #ccc !important;
}
}
div.progress-interface.ng-hide {
position: absolute;
width: 100%;
opacity: 0;
}
.post-save-results {
a.button{
float: left;
margin-right: 0.5em;
}
}
}
div.progress-bar {
height: 25px;
width: 30em;
max-width: 90%;
margin: 1em auto;
background: #f7f7f7;
padding: 3px;
border-radius: 0.3em;
border: 1px solid #eee;
span.progress-track{
display: block;
background: #b7ea53;
height: 100%;
border-radius: 0.3em;
box-shadow: inset 0 0 3px rgba(0,0,0,0.3);
transition: width 0.5s ease-in-out;
}
}

View File

@@ -2,12 +2,13 @@ require 'roo'
class Admin::ProductImportController < Spree::Admin::BaseController
before_filter :validate_upload_presence, except: :index
before_filter :validate_upload_presence, except: [:index, :process_data]
def import
# Save uploaded file to tmp directory
@filepath = save_uploaded_file(params[:file])
@importer = ProductImporter.new(File.new(@filepath), spree_current_user, params[:settings])
@original_filename = params[:file].try(:original_filename)
@import_into = params[:settings][:import_into]
check_file_errors @importer
@@ -17,10 +18,53 @@ class Admin::ProductImportController < Spree::Admin::BaseController
@shipping_categories = Spree::ShippingCategory.order('name ASC')
end
def save
@importer = ProductImporter.new(File.new(params[:filepath]), spree_current_user, params[:settings])
@importer.save_all if @importer.has_valid_entries?
@import_into = params[:settings][:import_into]
# def save
# @importer = ProductImporter.new(File.new(params[:filepath]), spree_current_user, params[:settings])
# @importer.save_all if @importer.has_valid_entries?
# @import_into = params[:settings][:import_into]
# end
def process_data
@importer = ProductImporter.new(File.new(params[:filepath]), spree_current_user, {start: params[:start], end: params[:end], import_into: params[:import_into]})
@importer.validate_entries
import_results = {
entries: @importer.entries_json,
reset_counts: @importer.reset_counts
}
render json: import_results
end
def save_data
@importer = ProductImporter.new(File.new(params[:filepath]), spree_current_user, {start: params[:start], end: params[:end], import_into: params[:import_into], settings: params[:settings]})
@importer.save_entries
save_results = {
results: {
products_created: @importer.products_created_count,
products_updated: @importer.products_updated_count,
inventory_created: @importer.inventory_created_count,
inventory_updated: @importer.inventory_updated_count,
products_reset: @importer.products_reset_count,
},
updated_ids: @importer.updated_ids,
errors: @importer.errors.full_messages
}
render json: save_results
end
def reset_absent_products
@importer = ProductImporter.new(File.new(params[:filepath]), spree_current_user, {import_into: params[:import_into], enterprises_to_reset: params[:enterprises_to_reset], updated_ids: params[:updated_ids], 'settings' => params[:settings]})
if params.has_key?(:enterprises_to_reset) and params.has_key?(:updated_ids)
@importer.reset_absent(params[:updated_ids])
end
render json: @importer.products_reset_count
end
private

View File

@@ -58,8 +58,8 @@ module Admin
@inventory_items = InventoryItem.where(enterprise_id: @hubs)
import_dates = [{id: '0', name: 'All'}]
inventory_import_dates.map {|i| import_dates.push({id: i, name: i.to_formatted_s(:long)}) }
@import_dates = import_dates.to_json
inventory_import_dates.map {|i| import_dates.push({id: i.to_date, name: i.to_date.to_formatted_s(:long)}) }
@import_dates = import_dates.uniq.to_json
end
def load_collection

View File

@@ -102,8 +102,8 @@ Spree::Admin::ProductsController.class_eval do
@producers = OpenFoodNetwork::Permissions.new(spree_current_user).managed_product_enterprises.is_primary_producer.by_name
@taxons = Spree::Taxon.order(:name)
import_dates = [{id: '0', name: ''}]
product_import_dates.map {|i| import_dates.push({id: i, name: i.to_formatted_s(:long)}) }
@import_dates = import_dates.to_json
product_import_dates.map {|i| import_dates.push({id: i.to_date, name: i.to_date.to_formatted_s(:long)}) }
@import_dates = import_dates.uniq.to_json
end
def product_import_dates

View File

@@ -5,7 +5,7 @@ class ProductImporter
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :total_supplier_products
attr_reader :total_supplier_products, :supplier_products, :updated_ids
def initialize(file, current_user, import_settings={})
if file.is_a?(File)
@@ -33,6 +33,7 @@ class ProductImporter
@inventory_permissions = {}
@total_supplier_products = 0
@supplier_products = {}
@reset_counts = {}
@updated_ids = []
@@ -42,16 +43,6 @@ class ProductImporter
end
end
def init_permissions
permissions = OpenFoodNetwork::Permissions.new(@current_user)
permissions.editable_enterprises.
order('is_primary_producer ASC, name').
map { |e| @editable_enterprises[e.name] = e.id }
@inventory_permissions = permissions.variant_override_enterprises_per_hub
end
def persisted?
false # ActiveModel
end
@@ -148,15 +139,55 @@ class ProductImporter
@current_user.admin? or ( @inventory_permissions[supplier_id] and @inventory_permissions[supplier_id].include? producer_id )
end
def validate_entries
@entries.each do |entry|
supplier_validation(entry)
if importing_into_inventory?
producer_validation(entry)
inventory_validation(entry)
else
category_validation(entry)
product_validation(entry)
end
end
end
def save_entries
validate_entries
save_all_valid
end
def reset_absent(updated_ids)
@products_created = updated_ids.count
@updated_ids = updated_ids
reset_absent_items
end
private
def init_product_importer
init_permissions
build_entries
if @import_settings.has_key?(:start) and @import_settings.has_key?(:end)
build_entries_in_range
else
build_entries
end
build_categories_index
build_suppliers_index
build_producers_index if importing_into_inventory?
validate_all
#validate_all
count_existing_items unless @import_settings.has_key?(:start)
end
def init_permissions
permissions = OpenFoodNetwork::Permissions.new(@current_user)
permissions.editable_enterprises.
order('is_primary_producer ASC, name').
map { |e| @editable_enterprises[e.name] = e.id }
@inventory_permissions = permissions.variant_override_enterprises_per_hub
end
def open_spreadsheet
@@ -184,6 +215,21 @@ class ProductImporter
end
end
def build_entries_in_range
start_line = @import_settings[:start]
end_line = @import_settings[:end]
(start_line..end_line).each do |i|
line_number = i + 1
row = @sheet.row(line_number)
row_data = Hash[[headers, row].transpose]
entry = SpreadsheetEntry.new(row_data)
entry.line_number = line_number
@entries.push entry
return if @sheet.last_row == line_number # TODO: test
end
end
def build_entries
rows.each_with_index do |row, i|
row_data = Hash[[headers, row].transpose]
@@ -212,7 +258,7 @@ class ProductImporter
end
def importing_into_inventory?
@import_settings['import_into'] == 'inventories'
@import_settings[:import_into] == 'inventories'
end
def inventory_validation(entry)
@@ -282,12 +328,7 @@ class ProductImporter
count
end
if @reset_counts[supplier_id]
@reset_counts[supplier_id][:existing_products] = products_count
else
@reset_counts[supplier_id] = {existing_products: products_count}
end
@supplier_products[supplier_id] = products_count
@total_supplier_products += products_count
end
end
@@ -415,7 +456,7 @@ class ProductImporter
self.errors.add(:importer, I18n.t(:product_importer_products_save_error)) if total_saved_count.zero?
reset_absent_items
reset_absent_items unless @import_settings.has_key?(:start)
total_saved_count
end
@@ -521,10 +562,10 @@ class ProductImporter
end
def reset_absent_items
return if total_saved_count.zero? or @updated_ids.empty?
return if total_saved_count.zero? or @updated_ids.empty? or !@import_settings.has_key?('settings')
enterprises_to_reset = []
@import_settings.each do |enterprise_id, settings|
@import_settings['settings'].each do |enterprise_id, settings|
enterprises_to_reset.push enterprise_id if settings['reset_all_absent'] and permission_by_id?(enterprise_id)
end
@@ -548,9 +589,9 @@ class ProductImporter
end
def assign_defaults(object, entry)
return unless @import_settings[entry.supplier_id.to_s] and @import_settings[entry.supplier_id.to_s]['defaults']
return unless @import_settings.has_key?(:settings) and @import_settings[:settings][entry.supplier_id.to_s] and @import_settings[:settings][entry.supplier_id.to_s]['defaults']
@import_settings[entry.supplier_id.to_s]['defaults'].each do |attribute, setting|
@import_settings[:settings][entry.supplier_id.to_s]['defaults'].each do |attribute, setting|
next unless setting['active']
case setting['mode']

View File

@@ -177,7 +177,7 @@ class AbilityDecorator
can [:admin, :index, :read, :search], Spree::Taxon
can [:admin, :index, :read, :create, :edit], Spree::Classification
can [:admin, :index, :import, :save], ProductImporter
can [:admin, :index, :import, :save, :save_data, :process_data, :reset_absent_products], ProductImporter
# Reports page
can [:admin, :index, :customers, :orders_and_distributors, :group_buys, :bulk_coop, :payments, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :packing], :report

View File

@@ -5,7 +5,7 @@
%th #{t('admin.product_import.import.line')}
- @importer.table_headings.each do |heading|
%th= heading
%tr{ng: {repeat: "(line_number, entry ) in entries | entriesFilterValid:'#{filter}' "}}
%tr{ng: {repeat: "(line_number, entry) in (entries | entriesFilterValid:'#{entries}')"}}
%td
%i{ng: {class: "{'fa fa-warning warning': (count(entry.errors) > 0), 'fa fa-check-circle success': (count(entry.errors) == 0)}"}}
%td

View File

@@ -1,4 +1,4 @@
%div.import-errors{ng: {controller: 'ImportFeedbackCtrl', repeat: "(line_number, entry ) in entries | entriesFilterValid:'invalid' "}}
%div.import-errors{ng: {controller: 'ImportFeedbackCtrl', repeat: "(line_number, entry ) in (entries | entriesFilterValid:'invalid')"}}
%p.line
%strong
#{t('admin.product_import.import.item_line')} {{line_number}}:

View File

@@ -3,91 +3,91 @@
%div{ng: {controller: 'ImportFeedbackCtrl'}}
%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', init: "all.count = count((entries | entriesFilterValid:'all')) "}}
%div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && all.count}'}}
%div.panel-section{ng: {controller: 'DropdownPanelsCtrl'}}
%div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count((entries | entriesFilterValid:"all"))}'}}
%div.header-caret
%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'all.count == 0'}}
%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count((entries | entriesFilterValid:"all")) == 0'}}
%div.header-icon.success
%i.fa.fa-info-circle.info
%div.header-count
%strong.item-count
{{all.count}}
{{count((entries | entriesFilterValid:"all"))}}
%div.header-description
#{t('admin.product_import.import.entries_found')}
%div.panel-content{ng: {hide: '!active || all.count == 0'}}
= render 'entries_table', entries: @importer.all_entries, filter: 'all'
%div.panel-content{ng: {hide: '!active || count((entries | entriesFilterValid:"all")) == 0'}}
= render 'entries_table', entries: 'all'
%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', init: "invalid.count = count((entries | entriesFilterValid:'invalid')) ", hide: 'invalid.count == 0'}}
%div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && invalid.count}'}}
%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', hide: 'count((entries | entriesFilterValid:"invalid")) == 0'}}
%div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count((entries | entriesFilterValid:"invalid"))}'}}
%div.header-caret
%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'invalid.count == 0'}}
%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count((entries | entriesFilterValid:"invalid")) == 0'}}
%div.header-icon.warning
%i.fa.fa-warning
%div.header-count
%strong.invalid-count
{{invalid.count}}
{{count((entries | entriesFilterValid:"invalid"))}}
%div.header-description
#{t('admin.product_import.import.entries_with_errors')}
%div.panel-content{ng: {hide: '!active || invalid.count == 0'}}
%div.panel-content{ng: {hide: '!active || count((entries | entriesFilterValid:"invalid")) == 0'}}
= render 'errors_list'
%br
= render 'entries_table', entries: @importer.all_entries, filter: 'invalid'
= render 'entries_table', entries: 'invalid'
%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', init: "create_product.count = count((entries | entriesFilterValid:'create_product')) ", hide: 'create_product.count == 0'}}
%div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && create_product.count}'}}
%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', hide: 'count((entries | entriesFilterValid:"create_product")) == 0'}}
%div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count((entries | entriesFilterValid:"create_product"))}'}}
%div.header-caret
%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'create_product.count == 0'}}
%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count((entries | entriesFilterValid:"create_product")) == 0'}}
%div.header-icon.success
%i.fa.fa-check-circle
%div.header-count
%strong.create-count
{{create_product.count}}
{{count((entries | entriesFilterValid:"create_product"))}}
%div.header-description
#{t('admin.product_import.import.products_to_create')}
%div.panel-content{ng: {hide: '!active || create_product.count == 0'}}
= render 'entries_table', entries: @importer.all_entries, filter: 'create_product'
%div.panel-content{ng: {hide: '!active || count((entries | entriesFilterValid:"create_product")) == 0'}}
= render 'entries_table', entries: 'create_product'
%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', init: "update_product.count = count((entries | entriesFilterValid:'update_product')) ", hide: 'update_product.count == 0'}}
%div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && update_product.count}'}}
%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', hide: 'count((entries | entriesFilterValid:"update_product")) == 0'}}
%div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count((entries | entriesFilterValid:"update_product"))}'}}
%div.header-caret
%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'update_product.count == 0'}}
%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count((entries | entriesFilterValid:"update_product")) == 0'}}
%div.header-icon.success
%i.fa.fa-check-circle
%div.header-count
%strong.update-count
{{update_product.count}}
{{count((entries | entriesFilterValid:"update_product"))}}
%div.header-description
#{t('admin.product_import.import.products_to_update')}
%div.panel-content{ng: {hide: '!active || update_product.count == 0'}}
= render 'entries_table', entries: @importer.all_entries, filter: 'update_product'
%div.panel-content{ng: {hide: '!active || count((entries | entriesFilterValid:"update_product")) == 0'}}
= render 'entries_table', entries: 'update_product'
%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', init: "create_inventory.count = count((entries | entriesFilterValid:'create_inventory')) ", hide: 'create_inventory.count == 0'}}
%div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && create_inventory.count}'}}
%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', hide: 'count((entries | entriesFilterValid:"create_inventory")) == 0'}}
%div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count((entries | entriesFilterValid:"create_inventory"))}'}}
%div.header-caret
%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'create_inventory.count == 0'}}
%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count((entries | entriesFilterValid:"create_inventory")) == 0'}}
%div.header-icon.success
%i.fa.fa-check-circle
%div.header-count
%strong.inv-create-count
{{create_inventory.count}}
{{count((entries | entriesFilterValid:"create_inventory"))}}
%div.header-description
#{t('admin.product_import.import.inventory_to_create')}
%div.panel-content{ng: {hide: '!active || create_inventory.count == 0'}}
= render 'entries_table', entries: @importer.all_entries, filter: 'create_inventory'
%div.panel-content{ng: {hide: '!active || count((entries | entriesFilterValid:"create_inventory")) == 0'}}
= render 'entries_table', entries: 'create_inventory'
%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', init: "update_inventory.count = count((entries | entriesFilterValid:'update_inventory')) ", hide: 'update_inventory.count == 0'}}
%div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && update_inventory.count}'}}
%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', hide: 'count((entries | entriesFilterValid:"update_inventory")) == 0'}}
%div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count((entries | entriesFilterValid:"update_inventory"))}'}}
%div.header-caret
%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'update_inventory.count == 0'}}
%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count((entries | entriesFilterValid:"update_inventory")) == 0'}}
%div.header-icon.success
%i.fa.fa-check-circle
%div.header-count
%strong.inv-update-count
{{update_inventory.count}}
{{count((entries | entriesFilterValid:"update_inventory"))}}
%div.header-description
#{t('admin.product_import.import.inventory_to_update')}
%div.panel-content{ng: {hide: '!active || update_inventory.count == 0'}}
= render 'entries_table', entries: @importer.all_entries, filter: 'update_inventory'
%div.panel-content{ng: {hide: '!active || count((entries | entriesFilterValid:"update_inventory")) == 0'}}
= render 'entries_table', entries: 'update_inventory'
%div.panel-section{ng: {controller: 'ImportOptionsFormCtrl', hide: 'resetTotal == 0'}}
%div.panel-header

View File

@@ -1,16 +1,19 @@
%table.import-settings{ng: {controller: 'ImportOptionsFormCtrl', init: "supplierId = #{supplier_id}; resetCount = #{@importer.reset_counts[supplier_id][:reset_count]}"}}
%table.import-settings{ng: {controller: 'ImportOptionsFormCtrl', init: "supplierId = #{supplier_id}; initForm()"}}
%tr
%td.description
#{t('admin.product_import.import.reset_absent?')}
%td
= check_box_tag "settings[#{supplier_id}][reset_all_absent]", 1, false, :'ng-model' => 'resetAbsent', :'ng-change' => 'toggleResetAbsent()'
= check_box_tag "settings[#{supplier_id}][reset_all_absent]", 1, false, 'ng-model' => "settings[#{supplier_id}]['reset_all_absent']", 'ng-change' => "toggleResetAbsent('#{supplier_id}')"
%td
%td
%td
%tr
%td.description
#{t('admin.product_import.import.default_stock')}
%td
= check_box_tag "settings[#{supplier_id}][defaults][count_on_hand][active]", 1, false, :'ng-model' => "count_on_hand_#{supplier_id}"
= check_box_tag "settings[#{supplier_id}][defaults][count_on_hand][active]", 1, false, 'ng-model' => "settings[#{supplier_id}]['defaults']['count_on_hand']['active']"
%td
= select_tag "settings[#{supplier_id}][defaults][count_on_hand][mode]", options_for_select({"#{t('admin.product_import.import.overwrite_all')}" => :overwrite_all, "#{t('admin.product_import.import.overwrite_empty')}" => :overwrite_empty}), {class: 'select2 fullwidth select2-no-search', 'ng-disabled' => "!count_on_hand_#{supplier_id}"}
%td
= number_field_tag "settings[#{supplier_id}][defaults][count_on_hand][value]", 0, 'ng-disabled' => "!count_on_hand_#{supplier_id}"
= select_tag "settings[#{supplier_id}][defaults][count_on_hand][mode]", options_for_select({"#{t('admin.product_import.import.overwrite_all')}" => :overwrite_all, "#{t('admin.product_import.import.overwrite_empty')}" => :overwrite_empty}), {class: 'select2 fullwidth select2-no-search', 'ng-model' => "settings[#{supplier_id}]['defaults']['count_on_hand']['mode']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['count_on_hand']['active']"}
%td
= number_field_tag "settings[#{supplier_id}][defaults][count_on_hand][value]", 0, 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['count_on_hand']['active']", 'ng-model' => "settings[#{supplier_id}]['defaults']['count_on_hand']['value']"

View File

@@ -1,44 +1,44 @@
%table.import-settings{ng: {controller: 'ImportOptionsFormCtrl', init: "supplierId = #{supplier_id}; resetCount = #{@importer.reset_counts[supplier_id][:reset_count]}"}}
%table.import-settings{ng: {controller: 'ImportOptionsFormCtrl', init: "supplierId = #{supplier_id}; initForm()"}}
%tr
%td.description
#{t('admin.product_import.import.reset_absent?')}
%td
= check_box_tag "settings[#{supplier_id}][reset_all_absent]", 1, false, :'ng-model' => 'resetAbsent', :'ng-change' => 'toggleResetAbsent()'
= check_box_tag "settings[#{supplier_id}][reset_all_absent]", 1, false, :'ng-model' => "settings[#{supplier_id}]['reset_all_absent']", :'ng-change' => "toggleResetAbsent('#{supplier_id}')"
%td
%td
%tr
%td.description
#{t('admin.product_import.import.default_stock')}
%td
= check_box_tag "settings[#{supplier_id}][defaults][on_hand][active]", 1, false, :'ng-model' => "on_hand_#{supplier_id}"
= check_box_tag "settings[#{supplier_id}][defaults][on_hand][active]", 1, false, 'ng-model' => "settings[#{supplier_id}]['defaults']['on_hand']['active']"
%td
= select_tag "settings[#{supplier_id}][defaults][on_hand][mode]", options_for_select({"#{t('admin.product_import.import.overwrite_all')}" => :overwrite_all, "#{t('admin.product_import.import.overwrite_empty')}" => :overwrite_empty}), {class: 'select2 fullwidth select2-no-search', 'ng-disabled' => "!on_hand_#{supplier_id}"}
= select_tag "settings[#{supplier_id}][defaults][on_hand][mode]", options_for_select({"#{t('admin.product_import.import.overwrite_all')}" => :overwrite_all, "#{t('admin.product_import.import.overwrite_empty')}" => :overwrite_empty}), {class: 'select2 fullwidth select2-no-search', 'ng-model' => "settings[#{supplier_id}]['defaults']['on_hand']['mode']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['on_hand']['active']"}
%td
= number_field_tag "settings[#{supplier_id}][defaults][on_hand][value]", 0, 'ng-disabled' => "!on_hand_#{supplier_id}"
= number_field_tag "settings[#{supplier_id}][defaults][on_hand][value]", 0, 'ng-model' => "settings[#{supplier_id}]['defaults']['on_hand']['value']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['on_hand']['active']"
%tr
%td.description
#{t('admin.product_import.import.default_tax_cat')}
%td
= check_box_tag "settings[#{supplier_id}][defaults][tax_category_id][active]", 1, false, :'ng-model' => "tax_category_id_#{supplier_id}"
= check_box_tag "settings[#{supplier_id}][defaults][tax_category_id][active]", 1, false, 'ng-model' => "settings[#{supplier_id}]['defaults']['tax_category_id']['active']"
%td
= select_tag "settings[#{supplier_id}][defaults][tax_category_id][mode]", options_for_select({"#{t('admin.product_import.import.overwrite_all')}" => :overwrite_all, "#{t('admin.product_import.import.overwrite_empty')}" => :overwrite_empty}), {class: 'select2 fullwidth select2-no-search', 'ng-disabled' => "!tax_category_id_#{supplier_id}"}
= select_tag "settings[#{supplier_id}][defaults][tax_category_id][mode]", options_for_select({"#{t('admin.product_import.import.overwrite_all')}" => :overwrite_all, "#{t('admin.product_import.import.overwrite_empty')}" => :overwrite_empty}), {class: 'select2 fullwidth select2-no-search', 'ng-model' => "settings[#{supplier_id}]['defaults']['tax_category_id']['mode']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['tax_category_id']['active']"}
%td
= select_tag "settings[#{supplier_id}][defaults][tax_category_id][value]", options_for_select(@tax_categories.map {|tc| [tc.name, tc.id]}), {prompt: 'None', class: 'select2 fullwidth select2-no-search', 'ng-disabled' => "!tax_category_id_#{supplier_id}"}
= select_tag "settings[#{supplier_id}][defaults][tax_category_id][value]", options_for_select(@tax_categories.map {|tc| [tc.name, tc.id]}), {prompt: 'None', class: 'select2 fullwidth select2-no-search', 'ng-model' => "settings[#{supplier_id}]['defaults']['tax_category_id']['value']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['tax_category_id']['active']"}
%tr
%td.description
#{t('admin.product_import.import.default_shipping_cat')}
%td
= check_box_tag "settings[#{supplier_id}][defaults][shipping_category_id][active]", 1, false, :'ng-model' => "shipping_category_id_#{supplier_id}"
= check_box_tag "settings[#{supplier_id}][defaults][shipping_category_id][active]", 1, false, 'ng-model' => "settings[#{supplier_id}]['defaults']['shipping_category_id']['active']"
%td
= select_tag "settings[#{supplier_id}][defaults][shipping_category_id][mode]", options_for_select({"#{t('admin.product_import.import.overwrite_all')}" => :overwrite_all, "#{t('admin.product_import.import.overwrite_empty')}" => :overwrite_empty}), {class: 'select2 fullwidth select2-no-search', 'ng-disabled' => "!shipping_category_id_#{supplier_id}"}
= select_tag "settings[#{supplier_id}][defaults][shipping_category_id][mode]", options_for_select({"#{t('admin.product_import.import.overwrite_all')}" => :overwrite_all, "#{t('admin.product_import.import.overwrite_empty')}" => :overwrite_empty}), {class: 'select2 fullwidth select2-no-search', 'ng-model' => "settings[#{supplier_id}]['defaults']['shipping_category_id']['mode']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['shipping_category_id']['active']"}
%td
= select_tag "settings[#{supplier_id}][defaults][shipping_category_id][value]", options_for_select(@shipping_categories.map {|sc| [sc.name, sc.id]}), {prompt: 'None', class: 'select2 fullwidth select2-no-search', 'ng-disabled' => "!shipping_category_id_#{supplier_id}"}
= select_tag "settings[#{supplier_id}][defaults][shipping_category_id][value]", options_for_select(@shipping_categories.map {|sc| [sc.name, sc.id]}), {prompt: 'None', class: 'select2 fullwidth select2-no-search', 'ng-model' => "settings[#{supplier_id}]['defaults']['shipping_category_id']['value']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['shipping_category_id']['active']"}
%tr
%td.description
#{t('admin.product_import.import.default_available_date')}
%td
= check_box_tag "settings[#{supplier_id}][defaults][available_on][active]", 1, false, :'ng-model' => "available_on_#{supplier_id}"
= check_box_tag "settings[#{supplier_id}][defaults][available_on][active]", 1, false, 'ng-model' => "settings[#{supplier_id}]['defaults']['available_on']['active']"
%td
= select_tag "settings[#{supplier_id}][defaults][available_on][mode]", options_for_select({"#{t('admin.product_import.import.overwrite_all')}" => :overwrite_all, "#{t('admin.product_import.import.overwrite_empty')}" => :overwrite_empty}), {class: 'select2 fullwidth select2-no-search', 'ng-disabled' => "!available_on_#{supplier_id}"}
= select_tag "settings[#{supplier_id}][defaults][available_on][mode]", options_for_select({"#{t('admin.product_import.import.overwrite_all')}" => :overwrite_all, "#{t('admin.product_import.import.overwrite_empty')}" => :overwrite_empty}), {class: 'select2 fullwidth select2-no-search', 'ng-model' => "settings[#{supplier_id}]['defaults']['available_on']['mode']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['available_on']['active']"}
%td
= text_field_tag "settings[#{supplier_id}][defaults][available_on][value]", nil, {class: 'datepicker', placeholder: 'Today', 'ng-disabled' => "!available_on_#{supplier_id}"}
= text_field_tag "settings[#{supplier_id}][defaults][available_on][value]", nil, {class: 'datepicker', placeholder: 'Today', 'ng-model' => "settings[#{supplier_id}]['defaults']['available_on']['value']", 'ng-disabled' => "!settings[#{supplier_id}]['defaults']['available_on']['active']"}

View File

@@ -0,0 +1,60 @@
%h5 #{t('admin.product_import.save.final_results')}
%br
%div.post-save-results
%p{ng: {show: 'updates.products_created'}}
%i.fa{ng: {class: "{'fa-info-circle': updates.products_created == 0, 'fa-check-circle': updates.products_created > 0}"}}
%strong.created-count
{{updates.products_created}}
#{t('admin.product_import.save.products_created')}
%p{ng: {show: 'updates.products_updated'}}
%i.fa{ng: {class: "{'fa-info-circle': updates.products_updated == 0, 'fa-check-circle': updates.products_updated > 0}"}}
%strong.updated-count
{{updates.products_updated}}
#{t('admin.product_import.save.products_updated')}
%p{ng: {show: 'updates.inventory_created'}}
%i.fa{ng: {class: "{'fa-info-circle': updates.inventory_created == 0, 'fa-check-circle': updates.inventory_created > 0}"}}
%strong.inv-created-count
{{updates.inventory_created}}
#{t('admin.product_import.save.inventory_created')}
%p{ng: {show: 'updates.inventory_updated'}}
%i.fa{ng: {class: "{'fa-info-circle': updates.inventory_updated == 0, 'fa-check-circle': updates.inventory_updated > 0}"}}
%strong.inv-updated-count
{{updates.inventory_updated}}
#{t('admin.product_import.save.inventory_updated')}
%p{ng: {show: 'updates.products_reset'}}
%i.fa.fa-info-circle
%strong.reset-count
{{updates.products_reset}}
- if @import_into == 'inventories'
#{t('admin.product_import.save.inventory_reset')}
- else
#{t('admin.product_import.save.products_reset')}
%br
%p{ng: {show: 'update_errors.length == 0'}}
#{t('admin.product_import.save.all_saved')}
%div{ng: {show: 'update_errors.length > 0'}}
%p {{updated_total}} #{t('admin.product_import.save.some_saved')}
%br
%h5 #{t('admin.product_import.save.save_errors')}
%p.save-error{ng: {repeat: 'error in update_errors'}}
&nbsp;-&nbsp; {{error}}
%br
%div{ng: {show: 'updated_total > 0'}}
- if @import_into == 'inventories'
%a.button.view{href: main_app.admin_inventory_path} #{t('admin.product_import.save.view_inventory')}
- else
%a.button.view{href: bulk_edit_admin_products_path + '?latest_import=true'} #{t('admin.product_import.save.view_products')}
%a.button{href: main_app.admin_product_import_path} #{t('admin.back')}

View File

@@ -14,4 +14,4 @@
%br
%br
%br
= submit_tag "#{t('admin.product_import.index.import')}"
= submit_tag "#{t('admin.product_import.index.upload')}"

View File

@@ -3,36 +3,62 @@
= render partial: 'spree/admin/shared/product_sub_menu'
= form_tag main_app.admin_product_import_save_path, {class: 'product-import', 'ng-app' => 'ofn.admin'} do
.import-wrapper{ng: {app: 'ofn.admin', controller: 'ImportFormCtrl', init: "supplier_product_counts = #{@importer.supplier_products.to_json}"}}
- if !@importer.has_valid_entries? #and @importer.invalid_count
- if @importer.item_count == 0 #and @importer.invalid_count
%h5 #{t('admin.product_import.import.no_valid_entries')}
%p #{t('admin.product_import.import.none_to_save')}
%br
- else
.progress-interface{ng: {show: 'step == "import"'}}
%span.filename
#{@original_filename}
%span.percentage
({{percentage}})
.progress-bar
%span.progress-track{class: 'ng-binding', style: "width:{{percentage}}"}
%button.start_import{ng: {click: 'start()', disabled: 'started', init: "item_count = #{@importer.item_count}; import_url = '#{main_app.admin_product_import_process_async_path}'; filepath = '#{@filepath}'; import_into = '#{@import_into}'"}}
#{t('admin.product_import.index.import')}
%button.review{ng: {click: 'viewResults()', disabled: '!finished'}}
#{t('admin.product_import.import.review')}
= render partial: "admin/json/injection_ams", locals: {ngModule: 'ofn.admin', name: 'productImportData', json: @importer.entries_json}
= form_tag false, {class: 'product-import', name: 'importForm', 'ng-show' => 'step == "results"'} do
= render 'import_options' if @importer.has_valid_entries?
= render 'import_options' if @importer.table_headings
= render 'import_review' if @importer.has_entries?
= render 'import_review' if @importer.table_headings
%div{ng: {controller: 'ImportFeedbackCtrl', show: "count((entries | entriesFilterValid:'valid')) > 0"}}
%div{ng: {if: "count((entries | entriesFilterValid:'invalid')) > 0"}}
%div{ng: {controller: 'ImportFeedbackCtrl', show: 'count((entries | entriesFilterValid:"valid")) > 0'}}
%div{ng: {if: 'count((entries | entriesFilterValid:"invalid")) > 0'}}
%br
%h5 #{t('admin.product_import.import.some_invalid_entries')}
%p #{t('admin.product_import.import.save_valid?')}
%div{ng: {show: 'count((entries | entriesFilterValid:"invalid")) == 0'}}
%br
%h5 #{t('admin.product_import.import.no_errors')}
%p #{t('admin.product_import.import.save_all_imported?')}
%br
%h5 #{t('admin.product_import.import.some_invalid_entries')}
%p #{t('admin.product_import.import.save_valid?')}
%div{ng: {show: "count((entries | entriesFilterValid:'invalid')) == 0"}}
= hidden_field_tag :filepath, @filepath
= hidden_field_tag "settings[import_into]", @import_into
%a.button{href: '', ng: {click: 'acceptResults()'}}
#{t('admin.product_import.import.proceed')}
%a.button{href: main_app.admin_product_import_path} #{t('admin.cancel')}
%div{ng: {controller: 'ImportFeedbackCtrl', show: 'count((entries | entriesFilterValid:"valid")) == 0'}}
%br
%h5 #{t('admin.product_import.import.no_errors')}
%p #{t('admin.product_import.import.save_all_imported?')}
%br
= hidden_field_tag :filepath, @filepath
= hidden_field_tag "settings[import_into]", @import_into
= submit_tag t('admin.save')
%a.button{href: main_app.admin_product_import_path} #{t('admin.cancel')}
%div{ng: {controller: 'ImportFeedbackCtrl', show: "count((entries | entriesFilterValid:'valid')) == 0"}}
%br
%a.button{href: main_app.admin_product_import_path} #{t('admin.cancel')}
%a.button{href: main_app.admin_product_import_path} #{t('admin.cancel')}
.progress-interface{ng: {show: 'step == "save"'}}
%span.filename
#{t('admin.product_import.import.save_imported')} ({{percentage}})
.progress-bar{}
%span.progress-track{ng: {style: "{'width':percentage}"}}
%button.start_save{ng: {click: 'start()', disabled: 'started', init: "item_count = #{@importer.item_count}; save_url = '#{main_app.admin_product_import_save_async_path}'; reset_url = '#{main_app.admin_product_import_reset_async_path}'; filepath = '#{@filepath}'; import_into = '#{@import_into}'"}}
#{t('admin.product_import.import.save')}
%button.view_results{ng: {click: 'finalResults()', disabled: '!finished'}}
#{t('admin.product_import.import.results')}
.save-results{ng: {show: 'step == "complete"'}}
= render 'save_results'

View File

@@ -500,7 +500,13 @@ en:
product_list: Product list
inventories: Inventories
import: Import
upload: Upload
import:
review: Review
proceed: Proceed
save: Save
results: Results
save_imported: Save imported products
no_valid_entries: No valid entries found
none_to_save: There are no entries that can be saved
some_invalid_entries: Imported file contains some invalid entries
@@ -538,8 +544,8 @@ en:
inventory_updated: Inventory items updated
products_reset: Products had stock level reset to zero
inventory_reset: Inventory items had stock level reset to zero
all_saved: "All %{num} items saved successfully"
total_saved: "%{num} items saved successfully"
all_saved: "All items saved successfully"
some_saved: "items saved successfully"
save_errors: Save errors
view_products: View Products
view_inventory: View Inventory

View File

@@ -135,7 +135,10 @@ Openfoodnetwork::Application.routes.draw do
get '/product_import', to: 'product_import#index'
post '/product_import', to: 'product_import#import'
post '/product_import/save', to: 'product_import#save', as: 'product_import_save'
post '/product_import/process_data', to: 'product_import#process_data', as: 'product_import_process_async'
post '/product_import/save_data', to: 'product_import#save_data', as: 'product_import_save_async'
post '/product_import/reset_absent', to: 'product_import#reset_absent_products', as: 'product_import_reset_async'
#post '/product_import/save', to: 'product_import#save', as: 'product_import_save'
resources :variant_overrides do
post :bulk_update, on: :collection

View File

@@ -43,14 +43,31 @@ feature "Product Import", js: true do
expect(page).to have_content "Select a spreadsheet to upload"
attach_file 'file', '/tmp/test.csv'
click_button 'Upload'
expect(page).to have_selector 'button.start_import'
expect(page).to have_selector "button.review[disabled='disabled']"
click_button 'Import'
wait_until { page.find("button.review:not([disabled='disabled'])").present? }
click_button 'Review'
expect(page).to have_selector '.item-count', text: "2"
expect(page).to_not have_selector '.invalid-count'
expect(page).to have_selector '.create-count', text: "2"
expect(page).to_not have_selector '.update-count'
click_link 'Proceed'
expect(page).to have_selector 'button.start_save'
expect(page).to have_selector "button.view_results[disabled='disabled']"
sleep 0.5
click_button 'Save'
wait_until { page.find("button.view_results:not([disabled='disabled'])").present? }
click_button 'Results'
expect(page).to have_selector '.created-count', text: '2'
expect(page).to_not have_selector '.updated-count'
@@ -61,6 +78,8 @@ feature "Product Import", js: true do
potatoes.price.should == 6.50
potatoes.variants.first.import_date.should be_within(1.minute).of DateTime.now
wait_until { page.find("a.button.view").present? }
click_link 'View Products'
expect(page).to have_content 'Bulk Edit Products'
@@ -69,36 +88,6 @@ feature "Product Import", js: true do
expect(page).to have_field "product_name", with: potatoes.name
end
it "displays info about invalid entries but still allows saving of valid entries" do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"]
csv << ["Good Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"]
csv << ["Bad Potatoes", "", "Vegetables", "6", "6.50", "1000", "", "1000"]
end
File.write('/tmp/test.csv', csv_data)
visit main_app.admin_product_import_path
expect(page).to have_content "Select a spreadsheet to upload"
attach_file('file', '/tmp/test.csv')
click_button 'Import'
expect(page).to have_selector '.item-count', text: "2"
expect(page).to have_selector '.invalid-count', text: "1"
expect(page).to have_selector '.create-count', text: "1"
expect(page).to have_selector 'input[type=submit][value="Save"]'
click_button 'Save'
expect(page).to have_selector '.created-count', text: '1'
Spree::Product.find_by_name('Bad Potatoes').should == nil
carrots = Spree::Product.find_by_name('Good Carrots')
carrots.supplier.should == enterprise
carrots.on_hand.should == 5
carrots.price.should == 3.20
end
it "displays info about invalid entries but no save button if all items are invalid" do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"]
@@ -111,7 +100,11 @@ feature "Product Import", js: true do
expect(page).to have_content "Select a spreadsheet to upload"
attach_file 'file', '/tmp/test.csv'
click_button 'Upload'
click_button 'Import'
wait_until { page.find("button.review:not([disabled='disabled'])").present? }
click_button 'Review'
expect(page).to have_selector '.item-count', text: "2"
expect(page).to have_selector '.invalid-count', text: "2"
@@ -121,75 +114,11 @@ feature "Product Import", js: true do
expect(page).to_not have_selector 'input[type=submit][value="Save"]'
end
it "can add new variants to existing products and update price and stock level of existing products" do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale", "display_name"]
csv << ["Hypothetical Cake", "Another Enterprise", "Cake", "5", "5.50", "500", "weight", "1", "Preexisting Banana"]
csv << ["Hypothetical Cake", "Another Enterprise", "Cake", "6", "3.50", "500", "weight", "1", "Emergent Coffee"]
end
File.write('/tmp/test.csv', csv_data)
visit main_app.admin_product_import_path
attach_file 'file', '/tmp/test.csv'
click_button 'Import'
expect(page).to have_selector '.item-count', text: "2"
expect(page).to_not have_selector '.invalid-count'
expect(page).to have_selector '.create-count', text: "1"
expect(page).to have_selector '.update-count', text: "1"
click_button 'Save'
expect(page).to have_selector '.created-count', text: '1'
expect(page).to have_selector '.updated-count', text: '1'
added_coffee = Spree::Variant.find_by_display_name('Emergent Coffee')
added_coffee.product.name.should == 'Hypothetical Cake'
added_coffee.price.should == 3.50
added_coffee.on_hand.should == 6
added_coffee.import_date.should be_within(1.minute).of DateTime.now
updated_banana = Spree::Variant.find_by_display_name('Preexisting Banana')
updated_banana.product.name.should == 'Hypothetical Cake'
updated_banana.price.should == 5.50
updated_banana.on_hand.should == 5
updated_banana.import_date.should be_within(1.minute).of DateTime.now
end
it "can add a new product and sub-variants of that product at the same time" do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale", "display_name"]
csv << ["Potatoes", "User Enterprise", "Vegetables", "5", "3.50", "500", "weight", "1000", "Small Bag"]
csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "5.50", "2000", "weight", "1000", "Big Bag"]
end
File.write('/tmp/test.csv', csv_data)
visit main_app.admin_product_import_path
attach_file 'file', '/tmp/test.csv'
click_button 'Import'
expect(page).to have_selector '.item-count', text: "2"
expect(page).to_not have_selector '.invalid-count'
expect(page).to have_selector '.create-count', text: "2"
click_button 'Save'
expect(page).to have_selector '.created-count', text: '2'
small_bag = Spree::Variant.find_by_display_name('Small Bag')
small_bag.product.name.should == 'Potatoes'
small_bag.price.should == 3.50
small_bag.on_hand.should == 5
big_bag = Spree::Variant.find_by_display_name('Big Bag')
big_bag.product.name.should == 'Potatoes'
big_bag.price.should == 5.50
big_bag.on_hand.should == 6
end
it "records a timestamp on import that can be viewed and filtered under Bulk Edit Products" do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"]
csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"]
csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1000", "weight", "1000"]
end
File.write('/tmp/test.csv', csv_data)
@@ -197,17 +126,29 @@ feature "Product Import", js: true do
expect(page).to have_content "Select a spreadsheet to upload"
attach_file 'file', '/tmp/test.csv'
click_button 'Upload'
click_button 'Import'
wait_until { page.find("button.review:not([disabled='disabled'])").present? }
click_button 'Review'
click_link 'Proceed'
sleep 0.5
click_button 'Save'
wait_until { page.find("button.view_results:not([disabled='disabled'])").present? }
click_button 'Results'
carrots = Spree::Product.find_by_name('Carrots')
carrots.variants.first.import_date.should be_within(1.minute).of DateTime.now
potatoes = Spree::Product.find_by_name('Potatoes')
potatoes.variants.first.import_date.should be_within(1.minute).of DateTime.now
click_link 'View Products'
wait_until { page.find("#p_#{carrots.id}").present? }
expect(page).to have_field "product_name", with: carrots.name
expect(page).to have_field "product_name", with: potatoes.name
find("div#columns-dropdown", :text => "COLUMNS").click
find("div#columns-dropdown div.menu div.menu_item", text: "Import").click
find("div#columns-dropdown", :text => "COLUMNS").click
@@ -217,10 +158,11 @@ feature "Product Import", js: true do
end
expect(page).to have_selector 'div#s2id_import_date_filter'
import_time = carrots.import_date.to_formatted_s(:long)
import_time = carrots.import_date.to_date.to_formatted_s(:long)
select import_time, from: "import_date_filter", visible: false
expect(page).to have_field "product_name", with: carrots.name
expect(page).to have_field "product_name", with: potatoes.name
expect(page).to_not have_field "product_name", with: product.name
expect(page).to_not have_field "product_name", with: product2.name
end
@@ -238,7 +180,11 @@ feature "Product Import", js: true do
attach_file 'file', '/tmp/test.csv'
select 'Inventories', from: "settings_import_into", visible: false
click_button 'Upload'
click_button 'Import'
wait_until { page.find("button.review:not([disabled='disabled'])").present? }
click_button 'Review'
expect(page).to have_selector '.item-count', text: "3"
expect(page).to_not have_selector '.invalid-count'
@@ -247,7 +193,11 @@ feature "Product Import", js: true do
expect(page).to have_selector '.inv-create-count', text: "2"
expect(page).to have_selector '.inv-update-count', text: "1"
click_link 'Proceed'
sleep 0.5
click_button 'Save'
wait_until { page.find("button.view_results:not([disabled='disabled'])").present? }
click_button 'Results'
expect(page).to_not have_selector '.created-count'
expect(page).to_not have_selector '.updated-count'
@@ -288,7 +238,7 @@ feature "Product Import", js: true do
visit main_app.admin_product_import_path
attach_file 'file', '/tmp/test.txt'
click_button 'Import'
click_button 'Upload'
expect(page).to have_content "Importer could not process file: invalid filetype"
expect(page).to_not have_selector 'input[type=submit][value="Save"]'
@@ -296,10 +246,9 @@ feature "Product Import", js: true do
File.delete('/tmp/test.txt')
end
it "returns and error if nothing was uploaded" do
it "returns an error if nothing was uploaded" do
visit main_app.admin_product_import_path
expect(page).to have_content 'Select a spreadsheet to upload'
click_button 'Import'
click_button 'Upload'
expect(flash_message).to eq I18n.t(:product_import_file_not_found_notice)
end
@@ -309,7 +258,7 @@ feature "Product Import", js: true do
visit main_app.admin_product_import_path
attach_file 'file', '/tmp/test.csv'
click_button 'Import'
click_button 'Upload'
expect(page).to_not have_selector '.create-count'
expect(page).to_not have_selector '.update-count'
@@ -333,7 +282,11 @@ feature "Product Import", js: true do
visit main_app.admin_product_import_path
attach_file 'file', '/tmp/test.csv'
click_button 'Upload'
click_button 'Import'
wait_until { page.find("button.review:not([disabled='disabled'])").present? }
click_button 'Review'
expect(page).to have_selector '.item-count', text: "2"
expect(page).to have_selector '.invalid-count', text: "1"
@@ -341,249 +294,16 @@ feature "Product Import", js: true do
expect(page.body).to have_content 'you do not have permission'
click_link 'Proceed'
sleep 0.5
click_button 'Save'
wait_until { page.find("button.view_results:not([disabled='disabled'])").present? }
click_button 'Results'
expect(page).to have_selector '.created-count', text: '1'
Spree::Product.find_by_name('My Carrots').should be_a Spree::Product
Spree::Product.find_by_name('Your Potatoes').should == nil
end
it "allows creating inventories for producers that a user's hub has permission for" do
csv_data = CSV.generate do |csv|
csv << ["name", "producer", "supplier", "category", "on_hand", "price", "unit_value"]
csv << ["Beans", "User Enterprise", "Another Enterprise", "Vegetables", "777", "3.20", "500"]
end
File.write('/tmp/test.csv', csv_data)
quick_login_as user2
visit main_app.admin_product_import_path
attach_file 'file', '/tmp/test.csv'
select 'Inventories', from: "settings_import_into", visible: false
click_button 'Import'
expect(page).to have_selector '.item-count', text: "1"
expect(page).to_not have_selector '.invalid-count'
expect(page).to have_selector '.inv-create-count', text: "1"
click_button 'Save'
expect(page).to have_selector '.inv-created-count', text: '1'
beans = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first
beans.count_on_hand.should == 777
end
it "does not allow creating inventories for producers that a user's hubs don't have permission for" do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "category", "on_hand", "price", "unit_value"]
csv << ["Beans", "User Enterprise", "Vegetables", "5", "3.20", "500"]
csv << ["Sprouts", "User Enterprise", "Vegetables", "6", "6.50", "500"]
end
File.write('/tmp/test.csv', csv_data)
quick_login_as user2
visit main_app.admin_product_import_path
attach_file 'file', '/tmp/test.csv'
select 'Inventories', from: "settings_import_into", visible: false
click_button 'Import'
expect(page).to have_selector '.item-count', text: "2"
expect(page).to have_selector '.invalid-count', text: "2"
expect(page).to_not have_selector '.inv-create-count'
expect(page.body).to have_content 'you do not have permission'
expect(page).to_not have_selector 'input[type=submit][value="Save"]'
end
end
describe "applying settings and defaults on import" do
before { quick_login_as_admin }
after { File.delete('/tmp/test.csv') }
it "can reset all products for an enterprise that are not present in the uploaded file to zero stock" do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"]
csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"]
csv << ["Beans", "User Enterprise", "Vegetables", "6", "6.50", "500", "weight", "1"]
end
File.write('/tmp/test.csv', csv_data)
visit main_app.admin_product_import_path
attach_file 'file', '/tmp/test.csv'
click_button 'Import'
expect(page).to have_selector '.item-count', text: "2"
expect(page).to_not have_selector '.invalid-count'
expect(page).to have_selector '.create-count', text: "1"
expect(page).to have_selector '.update-count', text: "1"
expect(page).to_not have_selector '.reset-count'
within 'div.import-settings' do
find('div.header-description').click # Import settings tab
check "settings_#{enterprise.id}_reset_all_absent"
end
expect(page).to have_selector '.reset-count', text: "2"
click_button 'Save'
expect(page).to have_selector '.created-count', text: '1'
expect(page).to have_selector '.updated-count', text: '1'
expect(page).to have_selector '.reset-count', text: '2'
Spree::Product.find_by_name('Carrots').on_hand.should == 5 # Present in file, added
Spree::Product.find_by_name('Beans').on_hand.should == 6 # Present in file, updated
Spree::Product.find_by_name('Sprouts').on_hand.should == 0 # In enterprise, not in file
Spree::Product.find_by_name('Cabbage').on_hand.should == 0 # In enterprise, not in file
Spree::Product.find_by_name('Lettuce').on_hand.should == 100 # In different enterprise; unchanged
end
it "can reset all inventory items for an enterprise that are not present in the uploaded file to zero stock" do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "producer", "category", "on_hand", "price", "unit_value"]
csv << ["Beans", "Another Enterprise", "User Enterprise", "Vegetables", "6", "3.20", "500"]
csv << ["Sprouts", "Another Enterprise", "User Enterprise", "Vegetables", "7", "6.50", "500"]
end
File.write('/tmp/test.csv', csv_data)
visit main_app.admin_product_import_path
attach_file 'file', '/tmp/test.csv'
select 'Inventories', from: "settings_import_into", visible: false
click_button 'Import'
expect(page).to have_selector '.item-count', text: "2"
expect(page).to_not have_selector '.invalid-count'
expect(page).to have_selector '.inv-create-count', text: "2"
expect(page).to_not have_selector '.reset-count'
within 'div.import-settings' do
find('div.header-description').click # Import settings tab
check "settings_#{enterprise2.id}_reset_all_absent"
end
expect(page).to have_selector '.reset-count', text: "1"
click_button 'Save'
expect(page).to have_selector '.inv-created-count', text: '2'
expect(page).to have_selector '.reset-count', text: '1'
beans = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first
sprouts = VariantOverride.where(variant_id: product3.variants.first.id, hub_id: enterprise2.id).first
cabbage = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first
lettuce = VariantOverride.where(variant_id: product5.variants.first.id, hub_id: enterprise.id).first
beans.count_on_hand.should == 6 # Present in file, created
sprouts.count_on_hand.should == 7 # Present in file, created
cabbage.count_on_hand.should == 0 # In enterprise, not in file (reset)
lettuce.count_on_hand.should == 96 # In different enterprise; unchanged
end
it "can overwrite fields with selected defaults when importing to product list" do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale", "tax_category_id", "available_on"]
csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1", tax_category.id, ""]
csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1000", "weight", "1000", "", ""]
end
File.write('/tmp/test.csv', csv_data)
visit main_app.admin_product_import_path
attach_file 'file', '/tmp/test.csv'
click_button 'Import'
within 'div.import-settings' do
find('div.header-description').click # Import settings tab
expect(page).to have_selector "#settings_#{enterprise.id}_defaults_on_hand_mode", visible: false
# Overwrite stock level of all items to 9000
check "settings_#{enterprise.id}_defaults_on_hand_active"
select 'Overwrite all', from: "settings_#{enterprise.id}_defaults_on_hand_mode", visible: false
fill_in "settings_#{enterprise.id}_defaults_on_hand_value", with: '9000'
# Overwrite default tax category, but only where field is empty
check "settings_#{enterprise.id}_defaults_tax_category_id_active"
select 'Overwrite if empty', from: "settings_#{enterprise.id}_defaults_tax_category_id_mode", visible: false
select tax_category2.name, from: "settings_#{enterprise.id}_defaults_tax_category_id_value", visible: false
# Set default shipping category (field not present in file)
check "settings_#{enterprise.id}_defaults_shipping_category_id_active"
select 'Overwrite all', from: "settings_#{enterprise.id}_defaults_shipping_category_id_mode", visible: false
select shipping_category.name, from: "settings_#{enterprise.id}_defaults_shipping_category_id_value", visible: false
# Set available_on date
check "settings_#{enterprise.id}_defaults_available_on_active"
select 'Overwrite all', from: "settings_#{enterprise.id}_defaults_available_on_mode", visible: false
find("input#settings_#{enterprise.id}_defaults_available_on_value").set '2020-01-01'
end
click_button 'Save'
expect(page).to have_selector '.created-count', text: '2'
carrots = Spree::Product.find_by_name('Carrots')
carrots.on_hand.should == 9000
carrots.tax_category_id.should == tax_category.id
carrots.shipping_category_id.should == shipping_category.id
carrots.available_on.should be_within(1.day).of(Time.zone.local(2020, 1, 1))
potatoes = Spree::Product.find_by_name('Potatoes')
potatoes.on_hand.should == 9000
potatoes.tax_category_id.should == tax_category2.id
potatoes.shipping_category_id.should == shipping_category.id
potatoes.available_on.should be_within(1.day).of(Time.zone.local(2020, 1, 1))
end
it "can overwrite fields with selected defaults when importing to inventory" do
csv_data = CSV.generate do |csv|
csv << ["name", "producer", "supplier", "category", "on_hand", "price", "unit_value"]
csv << ["Beans", "User Enterprise", "Another Enterprise", "Vegetables", "", "3.20", "500"]
csv << ["Sprouts", "User Enterprise", "Another Enterprise", "Vegetables", "7", "6.50", "500"]
csv << ["Cabbage", "User Enterprise", "Another Enterprise", "Vegetables", "", "1.50", "500"]
end
File.write('/tmp/test.csv', csv_data)
visit main_app.admin_product_import_path
attach_file 'file', '/tmp/test.csv'
select 'Inventories', from: "settings_import_into", visible: false
click_button 'Import'
within 'div.import-settings' do
find('div.header-description').click # Import settings tab
check "settings_#{enterprise2.id}_defaults_count_on_hand_active"
select 'Overwrite if empty', from: "settings_#{enterprise2.id}_defaults_count_on_hand_mode", visible: false
fill_in "settings_#{enterprise2.id}_defaults_count_on_hand_value", with: '9000'
end
expect(page).to have_selector '.item-count', text: "3"
expect(page).to_not have_selector '.invalid-count'
expect(page).to_not have_selector '.create-count'
expect(page).to_not have_selector '.update-count'
expect(page).to have_selector '.inv-create-count', text: "2"
expect(page).to have_selector '.inv-update-count', text: "1"
click_button 'Save'
expect(page).to_not have_selector '.created-count'
expect(page).to_not have_selector '.updated-count'
expect(page).to have_selector '.inv-created-count', text: '2'
expect(page).to have_selector '.inv-updated-count', text: '1'
beans_override = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first
sprouts_override = VariantOverride.where(variant_id: product3.variants.first.id, hub_id: enterprise2.id).first
cabbage_override = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first
beans_override.count_on_hand.should == 9000
sprouts_override.count_on_hand.should == 7
cabbage_override.count_on_hand.should == 9000
end
end
end

View File

@@ -6,25 +6,534 @@ describe ProductImporter do
let!(:admin) { create(:admin_user) }
let!(:user) { create_enterprise_user }
let!(:enterprise) { create(:enterprise, owner: user, name: "Test Enterprise") }
let!(:user2) { create_enterprise_user }
let!(:enterprise) { create(:enterprise, owner: user, name: "User Enterprise") }
let!(:enterprise2) { create(:distributor_enterprise, owner: user2, name: "Another Enterprise") }
let!(:relationship) { create(:enterprise_relationship, parent: enterprise, child: enterprise2, permissions_list: [:create_variant_overrides]) }
let!(:category) { create(:taxon, name: 'Vegetables') }
let!(:category2) { create(:taxon, name: 'Cake') }
let!(:tax_category) { create(:tax_category) }
let!(:tax_category2) { create(:tax_category) }
let!(:shipping_category) { create(:shipping_category) }
let!(:product) { create(:simple_product, supplier: enterprise2, name: 'Hypothetical Cake') }
let!(:variant) { create(:variant, product_id: product.id, price: '8.50', on_hand: '100', unit_value: '500', display_name: 'Preexisting Banana') }
let!(:product2) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Beans', unit_value: '500') }
let!(:product3) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Sprouts', unit_value: '500') }
let!(:product4) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Cabbage', unit_value: '500') }
let!(:product5) { create(:simple_product, supplier: enterprise2, on_hand: '100', name: 'Lettuce', unit_value: '500') }
let!(:variant_override) { create(:variant_override, variant_id: product4.variants.first.id, hub: enterprise2, count_on_hand: 42) }
let!(:variant_override2) { create(:variant_override, variant_id: product5.variants.first.id, hub: enterprise, count_on_hand: 96) }
let(:permissions) { OpenFoodNetwork::Permissions.new(user) }
describe "importing products from a spreadsheet" do
after { File.delete('/tmp/test-m.csv') }
it "validates the entries" do
before do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"]
csv << ["Carrots", "Test Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"]
csv << ["Potatoes", "Test Enterprise", "Vegetables", "6", "6.50", "1000", "weight", "1000"]
csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"]
csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1000", "weight", "1000"]
end
File.write('/tmp/test-m.csv', csv_data)
file = File.new('/tmp/test-m.csv')
@importer = ProductImporter.new(file, admin, {start: 1, end: 100, import_into: 'product_list'})
end
after { File.delete('/tmp/test-m.csv') }
it "returns the number of entries" do
expect(@importer.item_count).to eq(2)
end
it "validates entries and returns the results as json" do
@importer.validate_entries
entries = JSON.parse(@importer.entries_json)
expect(filter('valid', entries)).to eq 2
expect(filter('invalid', entries)).to eq 0
expect(filter('create_product', entries)).to eq 2
expect(filter('update_product', entries)).to eq 0
end
it "saves the results and returns info on updated products" do
@importer.save_entries
expect(@importer.products_created_count).to eq 2
expect(@importer.updated_ids).to be_a(Array)
expect(@importer.updated_ids.count).to eq 2
carrots = Spree::Product.find_by_name('Carrots')
carrots.supplier.should == enterprise
carrots.on_hand.should == 5
carrots.price.should == 3.20
carrots.variants.first.import_date.should be_within(1.minute).of DateTime.now
potatoes = Spree::Product.find_by_name('Potatoes')
potatoes.supplier.should == enterprise
potatoes.on_hand.should == 6
potatoes.price.should == 6.50
potatoes.variants.first.import_date.should be_within(1.minute).of DateTime.now
end
end
describe "when uploading a spreadsheet with some invalid entries" do
before do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"]
csv << ["Good Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"]
csv << ["Bad Potatoes", "", "Vegetables", "6", "6.50", "1000", "", "1000"]
end
File.write('/tmp/test-m.csv', csv_data)
file = File.new('/tmp/test-m.csv')
@importer = ProductImporter.new(file, admin, {start: 1, end: 100, import_into: 'product_list'})
end
after { File.delete('/tmp/test-m.csv') }
it "validates entries" do
@importer.validate_entries
entries = JSON.parse(@importer.entries_json)
expect(filter('valid', entries)).to eq 1
expect(filter('invalid', entries)).to eq 1
expect(filter('create_product', entries)).to eq 1
expect(filter('update_product', entries)).to eq 0
end
it "allows saving of the valid entries" do
@importer.save_entries
expect(@importer.products_created_count).to eq 1
expect(@importer.updated_ids).to be_a(Array)
expect(@importer.updated_ids.count).to eq 1
carrots = Spree::Product.find_by_name('Good Carrots')
carrots.supplier.should == enterprise
carrots.on_hand.should == 5
carrots.price.should == 3.20
carrots.variants.first.import_date.should be_within(1.minute).of DateTime.now
Spree::Product.find_by_name('Bad Potatoes').should == nil
end
end
describe "adding new variants to existing products and updating exiting products" do
before do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale", "display_name"]
csv << ["Hypothetical Cake", "Another Enterprise", "Cake", "5", "5.50", "500", "weight", "1", "Preexisting Banana"]
csv << ["Hypothetical Cake", "Another Enterprise", "Cake", "6", "3.50", "500", "weight", "1", "Emergent Coffee"]
end
File.write('/tmp/test-m.csv', csv_data)
file = File.new('/tmp/test-m.csv')
@importer = ProductImporter.new(file, admin, {start: 1, end: 100, import_into: 'product_list'})
end
after { File.delete('/tmp/test-m.csv') }
it "validates entries" do
@importer.validate_entries
entries = JSON.parse(@importer.entries_json)
expect(filter('valid', entries)).to eq 2
expect(filter('invalid', entries)).to eq 0
expect(filter('create_product', entries)).to eq 1
expect(filter('update_product', entries)).to eq 1
end
it "saves and updates" do
@importer.save_entries
expect(@importer.products_created_count).to eq 1
expect(@importer.products_updated_count).to eq 1
expect(@importer.updated_ids).to be_a(Array)
expect(@importer.updated_ids.count).to eq 2
added_coffee = Spree::Variant.find_by_display_name('Emergent Coffee')
added_coffee.product.name.should == 'Hypothetical Cake'
added_coffee.price.should == 3.50
added_coffee.on_hand.should == 6
added_coffee.import_date.should be_within(1.minute).of DateTime.now
updated_banana = Spree::Variant.find_by_display_name('Preexisting Banana')
updated_banana.product.name.should == 'Hypothetical Cake'
updated_banana.price.should == 5.50
updated_banana.on_hand.should == 5
updated_banana.import_date.should be_within(1.minute).of DateTime.now
end
end
describe "adding new product and sub-variant at the same time" do
before do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale", "display_name"]
csv << ["Potatoes", "User Enterprise", "Vegetables", "5", "3.50", "500", "weight", "1000", "Small Bag"]
csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "5.50", "2000", "weight", "1000", "Big Bag"]
end
File.write('/tmp/test-m.csv', csv_data)
file = File.new('/tmp/test-m.csv')
@importer = ProductImporter.new(file, admin, {start: 1, end: 100, import_into: 'product_list'})
end
after { File.delete('/tmp/test-m.csv') }
it "validates entries" do
@importer.validate_entries
entries = JSON.parse(@importer.entries_json)
expect(filter('valid', entries)).to eq 2
expect(filter('invalid', entries)).to eq 0
expect(filter('create_product', entries)).to eq 2
end
it "saves and updates" do
@importer.save_entries
expect(@importer.products_created_count).to eq 2
expect(@importer.updated_ids).to be_a(Array)
expect(@importer.updated_ids.count).to eq 2
small_bag = Spree::Variant.find_by_display_name('Small Bag')
small_bag.product.name.should == 'Potatoes'
small_bag.price.should == 3.50
small_bag.on_hand.should == 5
big_bag = Spree::Variant.find_by_display_name('Big Bag')
big_bag.product.name.should == 'Potatoes'
big_bag.price.should == 5.50
big_bag.on_hand.should == 6
end
end
describe "importing items into inventory" do
before do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "producer", "category", "on_hand", "price", "unit_value"]
csv << ["Beans", "Another Enterprise", "User Enterprise", "Vegetables", "5", "3.20", "500"]
csv << ["Sprouts", "Another Enterprise", "User Enterprise", "Vegetables", "6", "6.50", "500"]
csv << ["Cabbage", "Another Enterprise", "User Enterprise", "Vegetables", "2001", "1.50", "500"]
end
File.write('/tmp/test-m.csv', csv_data)
file = File.new('/tmp/test-m.csv')
@importer = ProductImporter.new(file, admin, {start: 1, end: 100, import_into: 'inventories'})
end
after { File.delete('/tmp/test-m.csv') }
it "validates entries" do
@importer.validate_entries
entries = JSON.parse(@importer.entries_json)
expect(filter('valid', entries)).to eq 3
expect(filter('invalid', entries)).to eq 0
expect(filter('create_inventory', entries)).to eq 2
expect(filter('update_inventory', entries)).to eq 1
end
it "saves and updates inventory" do
@importer.save_entries
expect(@importer.inventory_created_count).to eq 2
expect(@importer.inventory_updated_count).to eq 1
expect(@importer.updated_ids).to be_a(Array)
expect(@importer.updated_ids.count).to eq 3
beans_override = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first
sprouts_override = VariantOverride.where(variant_id: product3.variants.first.id, hub_id: enterprise2.id).first
cabbage_override = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first
Float(beans_override.price).should == 3.20
beans_override.count_on_hand.should == 5
Float(sprouts_override.price).should == 6.50
sprouts_override.count_on_hand.should == 6
Float(cabbage_override.price).should == 1.50
cabbage_override.count_on_hand.should == 2001
end
end
describe "handling enterprise permissions" do
after { File.delete('/tmp/test-m.csv') }
it "only allows product import into enterprises the user is permitted to manage" do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"]
csv << ["My Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"]
csv << ["Your Potatoes", "Another Enterprise", "Vegetables", "6", "6.50", "1000", "weight", "1000"]
end
File.write('/tmp/test-m.csv', csv_data)
file = File.new('/tmp/test-m.csv')
@importer = ProductImporter.new(file, user, {start: 1, end: 100, import_into: 'product_list'})
@importer.validate_entries
entries = JSON.parse(@importer.entries_json)
expect(filter('valid', entries)).to eq 1
expect(filter('invalid', entries)).to eq 1
expect(filter('create_product', entries)).to eq 1
@importer.save_entries
expect(@importer.products_created_count).to eq 1
expect(@importer.updated_ids).to be_a(Array)
expect(@importer.updated_ids.count).to eq 1
Spree::Product.find_by_name('My Carrots').should be_a Spree::Product
Spree::Product.find_by_name('Your Potatoes').should == nil
end
it "allows creating inventories for producers that a user's hub has permission for" do
csv_data = CSV.generate do |csv|
csv << ["name", "producer", "supplier", "category", "on_hand", "price", "unit_value"]
csv << ["Beans", "User Enterprise", "Another Enterprise", "Vegetables", "777", "3.20", "500"]
end
File.write('/tmp/test-m.csv', csv_data)
file = File.new('/tmp/test-m.csv')
@importer = ProductImporter.new(file, user2, {start: 1, end: 100, import_into: 'inventories'})
@importer.validate_entries
entries = JSON.parse(@importer.entries_json)
expect(filter('valid', entries)).to eq 1
expect(filter('invalid', entries)).to eq 0
expect(filter('create_inventory', entries)).to eq 1
@importer.save_entries
expect(@importer.inventory_created_count).to eq 1
expect(@importer.updated_ids).to be_a(Array)
expect(@importer.updated_ids.count).to eq 1
beans = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first
beans.count_on_hand.should == 777
end
it "does not allow creating inventories for producers that a user's hubs don't have permission for" do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "category", "on_hand", "price", "unit_value"]
csv << ["Beans", "User Enterprise", "Vegetables", "5", "3.20", "500"]
csv << ["Sprouts", "User Enterprise", "Vegetables", "6", "6.50", "500"]
end
File.write('/tmp/test-m.csv', csv_data)
file = File.new('/tmp/test-m.csv')
@importer = ProductImporter.new(file, user2, {start: 1, end: 100, import_into: 'inventories'})
@importer.validate_entries
entries = JSON.parse(@importer.entries_json)
expect(filter('valid', entries)).to eq 0
expect(filter('invalid', entries)).to eq 2
expect(filter('create_inventory', entries)).to eq 0
@importer.save_entries
expect(@importer.inventory_created_count).to eq 0
expect(@importer.updated_ids).to be_a(Array)
expect(@importer.updated_ids.count).to eq 0
end
end
describe "applying settings and defaults on import" do
after { File.delete('/tmp/test-m.csv') }
it "can reset all products for an enterprise that are not present in the uploaded file to zero stock" do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"]
csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"]
csv << ["Beans", "User Enterprise", "Vegetables", "6", "6.50", "500", "weight", "1"]
end
File.write('/tmp/test-m.csv', csv_data)
file = File.new('/tmp/test-m.csv')
importer = ProductImporter.new(file, admin)
@importer = ProductImporter.new(file, admin, {start: 1, end: 100, import_into: 'product_list', 'settings' => {enterprise.id => {'reset_all_absent' => true}}})
expect(importer.item_count).to eq(2)
@importer.validate_entries
entries = JSON.parse(@importer.entries_json)
expect(filter('valid', entries)).to eq 2
expect(filter('invalid', entries)).to eq 0
expect(filter('create_product', entries)).to eq 1
expect(filter('update_product', entries)).to eq 1
@importer.save_entries
expect(@importer.products_created_count).to eq 1
expect(@importer.products_updated_count).to eq 1
expect(@importer.updated_ids).to be_a(Array)
expect(@importer.updated_ids.count).to eq 2
@importer.reset_absent(@importer.updated_ids)
expect(@importer.products_reset_count).to eq 2
Spree::Product.find_by_name('Carrots').on_hand.should == 5 # Present in file, added
Spree::Product.find_by_name('Beans').on_hand.should == 6 # Present in file, updated
Spree::Product.find_by_name('Sprouts').on_hand.should == 0 # In enterprise, not in file
Spree::Product.find_by_name('Cabbage').on_hand.should == 0 # In enterprise, not in file
Spree::Product.find_by_name('Lettuce').on_hand.should == 100 # In different enterprise; unchanged
end
it "can reset all inventory items for an enterprise that are not present in the uploaded file to zero stock" do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "producer", "category", "on_hand", "price", "unit_value"]
csv << ["Beans", "Another Enterprise", "User Enterprise", "Vegetables", "6", "3.20", "500"]
csv << ["Sprouts", "Another Enterprise", "User Enterprise", "Vegetables", "7", "6.50", "500"]
end
File.write('/tmp/test-m.csv', csv_data)
file = File.new('/tmp/test-m.csv')
@importer = ProductImporter.new(file, admin, {start: 1, end: 100, import_into: 'inventories', 'settings' => {enterprise2.id => {'reset_all_absent' => true}}})
@importer.validate_entries
entries = JSON.parse(@importer.entries_json)
expect(filter('valid', entries)).to eq 2
expect(filter('invalid', entries)).to eq 0
expect(filter('create_inventory', entries)).to eq 2
@importer.save_entries
expect(@importer.inventory_created_count).to eq 2
expect(@importer.updated_ids).to be_a(Array)
expect(@importer.updated_ids.count).to eq 2
@importer.reset_absent(@importer.updated_ids)
expect(@importer.products_reset_count).to eq 1
beans = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first
sprouts = VariantOverride.where(variant_id: product3.variants.first.id, hub_id: enterprise2.id).first
cabbage = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first
lettuce = VariantOverride.where(variant_id: product5.variants.first.id, hub_id: enterprise.id).first
beans.count_on_hand.should == 6 # Present in file, created
sprouts.count_on_hand.should == 7 # Present in file, created
cabbage.count_on_hand.should == 0 # In enterprise, not in file (reset)
lettuce.count_on_hand.should == 96 # In different enterprise; unchanged
end
it "can overwrite fields with selected defaults when importing to product list" do
csv_data = CSV.generate do |csv|
csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale", "tax_category_id", "available_on"]
csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1", tax_category.id, ""]
csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1000", "weight", "1000", "", ""]
end
File.write('/tmp/test-m.csv', csv_data)
file = File.new('/tmp/test-m.csv')
import_settings = {enterprise.id.to_s => {
'defaults' => {
'on_hand' => {
'active' => true,
'mode' => 'overwrite_all',
'value' => '9000'
},
'tax_category_id' => {
'active' => true,
'mode' => 'overwrite_empty',
'value' => tax_category2.id
},
'shipping_category_id' => {
'active' => true,
'mode' => 'overwrite_all',
'value' => shipping_category.id
},
'available_on' => {
'active' => true,
'mode' => 'overwrite_all',
'value' => '2020-01-01'
}
}
}}
@importer = ProductImporter.new(file, admin, {start: 1, end: 100, import_into: 'product_list', settings: import_settings})
@importer.validate_entries
entries = JSON.parse(@importer.entries_json)
expect(filter('valid', entries)).to eq 2
expect(filter('invalid', entries)).to eq 0
expect(filter('create_product', entries)).to eq 2
@importer.save_entries
expect(@importer.products_created_count).to eq 2
expect(@importer.updated_ids).to be_a(Array)
expect(@importer.updated_ids.count).to eq 2
carrots = Spree::Product.find_by_name('Carrots')
carrots.on_hand.should == 9000
carrots.tax_category_id.should == tax_category.id
carrots.shipping_category_id.should == shipping_category.id
carrots.available_on.should be_within(1.day).of(Time.zone.local(2020, 1, 1))
potatoes = Spree::Product.find_by_name('Potatoes')
potatoes.on_hand.should == 9000
potatoes.tax_category_id.should == tax_category2.id
potatoes.shipping_category_id.should == shipping_category.id
potatoes.available_on.should be_within(1.day).of(Time.zone.local(2020, 1, 1))
end
it "can overwrite fields with selected defaults when importing to inventory" do
csv_data = CSV.generate do |csv|
csv << ["name", "producer", "supplier", "category", "on_hand", "price", "unit_value"]
csv << ["Beans", "User Enterprise", "Another Enterprise", "Vegetables", "", "3.20", "500"]
csv << ["Sprouts", "User Enterprise", "Another Enterprise", "Vegetables", "7", "6.50", "500"]
csv << ["Cabbage", "User Enterprise", "Another Enterprise", "Vegetables", "", "1.50", "500"]
end
File.write('/tmp/test-m.csv', csv_data)
file = File.new('/tmp/test-m.csv')
import_settings = {enterprise2.id.to_s => {
'defaults' => {
'count_on_hand' => {
'active' => true,
'mode' => 'overwrite_empty',
'value' => '9000'
}
}
}}
@importer = ProductImporter.new(file, admin, {start: 1, end: 100, import_into: 'inventories', settings: import_settings})
@importer.validate_entries
entries = JSON.parse(@importer.entries_json)
expect(filter('valid', entries)).to eq 3
expect(filter('invalid', entries)).to eq 0
expect(filter('create_inventory', entries)).to eq 2
expect(filter('update_inventory', entries)).to eq 1
@importer.save_entries
expect(@importer.inventory_created_count).to eq 2
expect(@importer.inventory_updated_count).to eq 1
expect(@importer.updated_ids).to be_a(Array)
expect(@importer.updated_ids.count).to eq 3
beans_override = VariantOverride.where(variant_id: product2.variants.first.id, hub_id: enterprise2.id).first
sprouts_override = VariantOverride.where(variant_id: product3.variants.first.id, hub_id: enterprise2.id).first
cabbage_override = VariantOverride.where(variant_id: product4.variants.first.id, hub_id: enterprise2.id).first
beans_override.count_on_hand.should == 9000
sprouts_override.count_on_hand.should == 7
cabbage_override.count_on_hand.should == 9000
end
end
end
private
def filter(type, entries)
valid_count = 0
entries.each do |line_number, entry|
validates_as = entry['validates_as']
valid_count += 1 if type == 'valid' and (validates_as != '')
valid_count += 1 if type == 'invalid' and (validates_as == '')
valid_count += 1 if type == 'create_product' and (validates_as == 'new_product' or validates_as == 'new_variant')
valid_count += 1 if type == 'update_product' and validates_as == 'existing_variant'
valid_count += 1 if type == 'create_inventory' and validates_as == 'new_inventory_item'
valid_count += 1 if type == 'update_inventory' and validates_as == 'existing_inventory_item'
end
valid_count
end