Product Import options and defaults

Added available_on test

Obscure case fix and extra spec
This commit is contained in:
Matt-Yorkley
2017-03-04 20:51:41 +00:00
committed by Rob Harrington
parent f4511fc74d
commit 14fb40a996
10 changed files with 269 additions and 45 deletions

View File

@@ -1,4 +1,4 @@
angular.module("ofn.admin").controller "FeedbackPanelsCtrl", ($scope) ->
angular.module("ofn.admin").controller "DropdownPanelsCtrl", ($scope) ->
$scope.active = false
$scope.togglePanel = ->

View File

@@ -1,6 +1,6 @@
div.feedback-section {
div.panel-section {
div.feedback-header {
div.panel-header {
width: 100%;
//font-size: 1.5em;
clear: both;
@@ -22,7 +22,7 @@ div.feedback-section {
div.header-icon {
width: 2.5em;
text-align: center;
padding-top: 0.1em;
padding-top: 0.18em;
.fa {
font-size: 1.5em;
@@ -48,17 +48,17 @@ div.feedback-section {
}
div.feedback-header:hover {
div.panel-header:hover {
cursor: pointer;
background-color: #f7f7f7;
}
div.feedback-header.active {
div.panel-header.active {
background-color: #efefef;
text-shadow: 1px 1px 0px rgba(255,255,255,0.75);
}
div.feedback-panel {
div.panel-content {
width: 100%;
clear: both;
//border: 1px solid #ccc;
@@ -102,3 +102,61 @@ div.feedback-section {
br.panels.clearfix {
clear: both;
}
table.import-settings {
background-color: transparent !important;
width: auto;
//select {
// width: 100%;
//}
tr {
}
tbody tr:hover td {
background-color: #f3f3f3;
}
td {
border: 0;
border-bottom: 1px solid #eee;
text-align: left;
input {
width: 15em;
}
input[type="checkbox"] {
width: auto;
}
}
td.description {
font-weight: bold;
padding-right: 2.5em;
}
tr:first-child td {
//border-top: 1px solid #eee;
border-top: 0;
}
tr:last-child td {
//border-top: 1px solid #eee;
border-bottom: 0;
}
}
.panel-section.import-settings {
.header-icon {
color: #BFBFBF;
}
.header-description {
padding-left: 1em;
}
}
.select2-search {
display: none;
}
.select2-results {
margin: 0;
}

View File

@@ -17,7 +17,7 @@ class Admin::ProductImportController < Spree::Admin::BaseController
end
def save
@importer = ProductImporter.new(File.new(params[:filepath]), editable_enterprises)
@importer = ProductImporter.new(File.new(params[:filepath]), editable_enterprises, params[:settings])
@importer.save_all if @importer.has_valid_entries?
end

View File

@@ -5,10 +5,9 @@ class ProductImporter
include ActiveModel::Conversion
include ActiveModel::Validations
def initialize(file, editable_enterprises, options={})
def initialize(file, editable_enterprises, import_settings={})
if file.is_a?(File)
@file = file
@options = options
@sheet = open_spreadsheet
@valid_entries = {}
@invalid_entries = {}
@@ -21,10 +20,11 @@ class ProductImporter
@variants_created = 0
@variants_updated = 0
@import_settings = import_settings
@editable_enterprises = {}
editable_enterprises.map { |e| @editable_enterprises[e.name] = e.id }
@non_display_attributes = 'id', 'product_id', 'variant_id', 'supplier_id', 'primary_taxon_id', 'category_id', 'shipping_category_id', 'tax_category_id'
@non_display_attributes = 'id', 'product_id', 'variant_id', 'supplier_id', 'primary_taxon_id', 'category_id', 'shipping_category_id', 'tax_category_id', 'on_hand_nil'
validate_all if @sheet
else
@@ -144,12 +144,8 @@ class ProductImporter
supplier_validation(line_number, entry)
category_validation(line_number, entry)
# Ensure on_hand isn't nil because Spree::Product and
# Spree::Variant each validate it differently
entry['on_hand'] = 0 if entry['on_hand'].nil?
set_update_status(line_number, entry)
mark_as_valid(line_number, entry) unless entry_invalid?(line_number)
end
@@ -264,14 +260,9 @@ class ProductImporter
product = Spree::Product.new()
product.assign_attributes(entry.except('id'))
assign_defaults(product, entry)
if product.save
# Ensure display_name and on_demand are copied to new variant
if entry['display_name'] || entry['on_demand']
variant = product.variants.first
variant.display_name = entry['display_name'] if entry['display_name']
variant.on_demand = entry['on_demand'] if entry['on_demand']
variant.save
end
ensure_variant_updated(entry, product)
@products_created += 1
else
self.errors.add("Line #{line_number}:", product.errors.full_messages)
@@ -282,6 +273,7 @@ class ProductImporter
@variants_to_update.each do |line_number, data|
variant = data[:variant]
assign_defaults(variant, data[:entry])
if variant.valid? and variant.save
@variants_updated += 1
else
@@ -291,6 +283,7 @@ class ProductImporter
@variants_to_create.each do |line_number, data|
new_variant = data[:variant]
assign_defaults(new_variant, data[:entry])
if new_variant.valid? and new_variant.save
@variants_created += 1
else
@@ -303,6 +296,29 @@ class ProductImporter
total_saved_count
end
def assign_defaults(object, entry)
@import_settings[entry['supplier_id'].to_s]['defaults'].each do |attribute, setting|
case setting['mode']
when 'overwrite_all'
object.assign_attributes(attribute => setting['value'])
when 'overwrite_empty'
if object.send(attribute).blank? or (attribute == 'on_hand' and entry['on_hand_nil'])
object.assign_attributes(attribute => setting['value'])
end
end
end
end
def ensure_variant_updated(entry, product)
# Ensure display_name and on_demand are copied to new product's variant
if entry['display_name'] || entry['on_demand']
variant = product.variants.first
variant.display_name = entry['display_name'] if entry['display_name']
variant.on_demand = entry['on_demand'] if entry['on_demand']
variant.save
end
end
def set_update_status(line_number, entry)
# Find product with matching supplier and name
match = Spree::Product.where(supplier_id: entry['supplier_id'], name: entry['name'], deleted_at: nil).first
@@ -337,6 +353,7 @@ class ProductImporter
def mark_as_existing_variant(line_number, entry, existing_variant)
existing_variant.assign_attributes(entry.except('id', 'product_id'))
check_on_hand_nil(entry, existing_variant)
if existing_variant.valid?
@variants_to_update[line_number] = {entry: entry, variant: existing_variant} unless entry_invalid?(line_number)
else
@@ -347,6 +364,7 @@ class ProductImporter
def mark_as_new_variant(line_number, entry, product_id)
new_variant = Spree::Variant.new(entry.except('id', 'product_id'))
new_variant.product_id = product_id
check_on_hand_nil(entry, new_variant)
if new_variant.valid?
@variants_to_create[line_number] = {entry: entry, variant: new_variant} unless entry_invalid?(line_number)
else
@@ -354,6 +372,13 @@ class ProductImporter
end
end
def check_on_hand_nil(entry, variant)
if entry['on_hand'].blank?
variant.on_hand = 0
entry['on_hand_nil'] = true
end
end
def delete_uploaded_file
# Only delete if file is in '/tmp/product_import' directory
if @file.path == Rails.root.join('tmp', 'product_import').to_s

View File

@@ -0,0 +1,21 @@
%h5 Import options and defaults
%br
- @importer.suppliers_index.each do |name, id|
- if name and id
%div.panel-section.import-settings{ng: {controller: 'DropdownPanelsCtrl'}}
%div.panel-header{ng: {click: 'togglePanel()', class: '{active: active}'}}
%div.header-caret
%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}"}}
%div.header-icon
%i.fa.fa-edit
-#%div.header-count
-# %strong.invalid-count= @importer.invalid_count
%div.header-description
= name
%div.panel-content{ng: {hide: '!active'}}
= render 'options_form', id: id, name: name
%br.panels.clearfix
%br

View File

@@ -1,8 +1,8 @@
%h5 Import validation overview
%br
%div.feedback-section
%div.feedback-header
%div.panel-section
%div.panel-header
%div.header-caret
-#%i.icon-chevron-right{ng: {hide: 'active'}}
-#%i.icon-chevron-down{ng: {hide: '!active'}}
@@ -12,51 +12,48 @@
%strong.item-count= @importer.item_count
%div.header-description
Entries found in imported file
-#%div.feedback-panel{ng: {hide: '!active'}}
-#%div.panel-content{ng: {hide: '!active'}}
-# Content goes here
%div.feedback-section{ng: {controller: 'FeedbackPanelsCtrl', init: "count = #{@importer.invalid_count}"}}
%div.feedback-header{ng: {click: 'togglePanel()', class: '{active: active && count}'}}
%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', init: "count = #{@importer.invalid_count}"}}
%div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count}'}}
%div.header-caret
%i.icon-chevron-right{ng: {hide: 'active || count == 0'}}
%i.icon-chevron-down{ng: {hide: '!active || count == 0'}}
%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count == 0'}}
%div.header-icon
%i.fa.fa-warning
%div.header-count
%strong.invalid-count= @importer.invalid_count
%div.header-description
Items contain errors and will not be imported
%div.feedback-panel{ng: {hide: '!active || count == 0'}}
%div.panel-content{ng: {hide: '!active || count == 0'}}
= render 'errors_list'
%br
= render 'entries_table', entries: @importer.invalid_entries
%div.feedback-section{ng: {controller: 'FeedbackPanelsCtrl', init: "count = #{@importer.products_create_count}"}}
%div.feedback-header{ng: {click: 'togglePanel()', class: '{active: active && count}'}}
%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', init: "count = #{@importer.products_create_count}"}}
%div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count}'}}
%div.header-caret
%i.icon-chevron-right{ng: {hide: 'active || count == 0'}}
%i.icon-chevron-down{ng: {hide: '!active || count == 0'}}
%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count == 0'}}
%div.header-icon
%i.fa.fa-check-circle
%div.header-count
%strong.create-count= @importer.products_create_count
%div.header-description
Products will be created
%div.feedback-panel{ng: {hide: '!active || count == 0'}}
%div.panel-content{ng: {hide: '!active || count == 0'}}
= render 'entries_table', entries: @importer.products_to_create
%div.feedback-section{ng: {controller: 'FeedbackPanelsCtrl', init: "count = #{@importer.products_update_count}"}}
%div.feedback-header{ng: {click: 'togglePanel()', class: '{active: active && count}'}}
%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', init: "count = #{@importer.products_update_count}"}}
%div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count}'}}
%div.header-caret
%i.icon-chevron-right{ng: {hide: 'active || count == 0'}}
%i.icon-chevron-down{ng: {hide: '!active || count == 0'}}
%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count == 0'}}
%div.header-icon
%i.fa.fa-check-circle
%div.header-count
%strong.update-count= @importer.products_update_count
%div.header-description
Products will be updated
%div.feedback-panel{ng: {hide: '!active || count == 0'}}
%div.panel-content{ng: {hide: '!active || count == 0'}}
= render 'entries_table', entries: @importer.products_to_update
%br.panels.clearfix

View File

@@ -0,0 +1,35 @@
%table.import-settings
%tr
%td.description
Remove absent products?
%td
= check_box_tag "settings[#{id}][products_absent]", 1, false
%td
%tr
%td.description
Set default stock level
%td
= select_tag "settings[#{id}][defaults][on_hand][mode]", options_for_select({"Don't overwrite" => :overwrite_none, "Overwrite all" => :overwrite_all, "Overwrite if empty" => :overwrite_empty}), {class: 'select2 fullwidth'}
%td
= number_field_tag "settings[#{id}][defaults][on_hand][value]", 0
%tr
%td.description
Set default tax category
%td
= select_tag "settings[#{id}][defaults][tax_category_id][mode]", options_for_select({"Don't overwrite" => :overwrite_none, "Overwrite all" => :overwrite_all, "Overwrite if empty" => :overwrite_empty}), {class: 'select2 fullwidth'}
%td
= select_tag "settings[#{id}][defaults][tax_category_id][value]", options_for_select(@tax_categories.map {|tc| [tc.name, tc.id]}), {prompt: 'None', class: 'select2 fullwidth'}
%tr
%td.description
Set default shipping category
%td
= select_tag "settings[#{id}][defaults][shipping_category_id][mode]", options_for_select({"Don't overwrite" => :overwrite_none, "Overwrite all" => :overwrite_all, "Overwrite if empty" => :overwrite_empty}), {class: 'select2 fullwidth'}
%td
= select_tag "settings[#{id}][defaults][shipping_category_id][value]", options_for_select(@shipping_categories.map {|sc| [sc.name, sc.id]}), {prompt: 'None', class: 'select2 fullwidth'}
%tr
%td.description
Set default available date
%td
= select_tag "settings[#{id}][defaults][available_on][mode]", options_for_select({"Don't overwrite" => :overwrite_none, "Overwrite all" => :overwrite_all, "Overwrite if empty" => :overwrite_empty}), {class: 'select2 fullwidth'}
%td
= text_field_tag "settings[#{id}][defaults][available_on][value]", nil, {class: 'datepicker', placeholder: 'Today'}

View File

@@ -10,6 +10,8 @@
%p There are no entries that can be saved
%br
= render 'import_options' if @importer.has_valid_entries?
= render 'import_review'
- if @importer.has_valid_entries?

View File

@@ -15,8 +15,8 @@
%br
- if !@importer.errors.full_messages
%h5 All #{importer.total_saved_count} items saved successfully
- if @importer.errors.count == 0
%h5 All #{@importer.total_saved_count} items saved successfully
- else
%h5 Errors
- @importer.errors.full_messages.each do |error|

View File

@@ -11,9 +11,11 @@ feature "Product Import", js: true do
let!(:enterprise2) { create(:supplier_enterprise, owner: admin, name: "Another Enterprise") }
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: enterprise, name: 'Hypothetical Cake') }
let!(:variant) { create(:variant, product_id: product.id, price: '8.50', count_on_hand: '100', unit_value: '500', display_name: 'Preexisting Banana') }
let(:permissions) { OpenFoodNetwork::Permissions.new(user) }
describe "when importing products from uploaded file" do
before { quick_login_as_admin }
@@ -77,7 +79,7 @@ feature "Product Import", js: true do
carrots.price.should == 3.20
end
it "displays info about invalid entries but no save button if all invalid" do
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"]
csv << ["Bad Carrots", "Unkown Enterprise", "Mouldy vegetables", "666", "3.20", "", "weight", ""]
@@ -130,6 +132,37 @@ feature "Product Import", js: true do
updated_banana.price.should == 5.50
updated_banana.on_hand.should == 5
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 have_selector '.invalid-count', text: "0"
expect(page).to have_selector '.create-count', text: "2"
expect(page).to have_selector '.update-count', text: "0"
click_button 'Save'
expect(page).to have_content "Products created: 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 "when dealing with uploaded files" do
@@ -200,4 +233,57 @@ feature "Product Import", js: true do
end
end
describe "applying settings and defaults on import" do
before { quick_login_as_admin }
it "overwrites fields with selected defaults" 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
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
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)
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
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_content "Products created: 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
end
end