mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-28 21:07:16 +00:00
Merge tag 'v1.8.8' into transifex
This commit is contained in:
1
Gemfile
1
Gemfile
@@ -63,6 +63,7 @@ gem 'wkhtmltopdf-binary'
|
||||
|
||||
gem 'foreigner'
|
||||
gem 'immigrant'
|
||||
gem 'roo', '~> 2.7.0'
|
||||
|
||||
gem 'whenever', require: false
|
||||
|
||||
|
||||
@@ -558,6 +558,9 @@ GEM
|
||||
roadie-rails (1.0.3)
|
||||
rails (>= 3.0, < 4.2)
|
||||
roadie (~> 3.0)
|
||||
roo (2.7.1)
|
||||
nokogiri (~> 1)
|
||||
rubyzip (~> 1.1, < 2.0.0)
|
||||
rspec (2.14.1)
|
||||
rspec-core (~> 2.14.0)
|
||||
rspec-expectations (~> 2.14.0)
|
||||
@@ -576,6 +579,7 @@ GEM
|
||||
rspec-retry (0.4.2)
|
||||
rspec-core
|
||||
ruby-progressbar (1.7.1)
|
||||
rubyzip (1.2.0)
|
||||
safe_yaml (0.9.5)
|
||||
sass (3.3.14)
|
||||
sass-rails (3.2.6)
|
||||
@@ -717,6 +721,7 @@ DEPENDENCIES
|
||||
redcarpet
|
||||
representative_view
|
||||
roadie-rails (~> 1.0.3)
|
||||
roo (~> 2.7.0)
|
||||
rspec-rails
|
||||
rspec-retry
|
||||
sass (~> 3.3)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
angular.module("ofn.admin").controller "DropdownPanelsCtrl", ($scope) ->
|
||||
$scope.active = false
|
||||
|
||||
$scope.togglePanel = ->
|
||||
$scope.active = !$scope.active
|
||||
@@ -0,0 +1,15 @@
|
||||
angular.module("ofn.admin").controller "ImportOptionsFormCtrl", ($scope, $rootScope, ProductImportService) ->
|
||||
|
||||
$scope.toggleResetAbsent = () ->
|
||||
confirmed = confirm 'This will set stock level to zero on all products for this \n' +
|
||||
'enterprise that are not present in the uploaded file.' if $scope.resetAbsent
|
||||
|
||||
if confirmed or !$scope.resetAbsent
|
||||
ProductImportService.updateResetAbsent($scope.supplierId, $scope.resetCount, $scope.resetAbsent)
|
||||
else
|
||||
$scope.resetAbsent = false
|
||||
|
||||
$scope.resetTotal = ProductImportService.resetTotal
|
||||
|
||||
$rootScope.$watch 'resetTotal', (newValue) ->
|
||||
$scope.resetTotal = newValue if newValue || newValue == 0
|
||||
@@ -0,0 +1,15 @@
|
||||
angular.module("ofn.admin").factory "ProductImportService", ($rootScope) ->
|
||||
new class ProductImportService
|
||||
suppliers: {}
|
||||
resetTotal: 0
|
||||
|
||||
updateResetAbsent: (supplierId, resetCount, resetAbsent) ->
|
||||
if resetAbsent
|
||||
@suppliers[supplierId] = resetCount
|
||||
@resetTotal += resetCount
|
||||
else
|
||||
@suppliers[supplierId] = null
|
||||
@resetTotal -= resetCount
|
||||
|
||||
$rootScope.resetTotal = @resetTotal
|
||||
|
||||
225
app/assets/stylesheets/admin/product_import.css.scss
Normal file
225
app/assets/stylesheets/admin/product_import.css.scss
Normal file
@@ -0,0 +1,225 @@
|
||||
div.panel-section {
|
||||
|
||||
.neutral {
|
||||
color: #bfbfbf;
|
||||
}
|
||||
.warning {
|
||||
color: #da5354;
|
||||
}
|
||||
.success {
|
||||
color: #86d83a;
|
||||
}
|
||||
.info {
|
||||
color: #68b7c0;
|
||||
}
|
||||
|
||||
div.panel-header {
|
||||
width: 100%;
|
||||
//font-size: 1.5em;
|
||||
clear: both;
|
||||
//border: 1px solid #ccc;
|
||||
float: left;
|
||||
padding: 0.5em;
|
||||
|
||||
div {
|
||||
font-size: 1.25em;
|
||||
float: left;
|
||||
}
|
||||
|
||||
div.header-caret {
|
||||
width: 2em;
|
||||
text-align: center;
|
||||
min-height: 0.1em; //Empty div fix
|
||||
}
|
||||
|
||||
div.header-icon {
|
||||
width: 2.5em;
|
||||
text-align: center;
|
||||
padding-top: 0.18em;
|
||||
|
||||
i {
|
||||
font-size: 1.5em;
|
||||
line-height: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
div.header-count {
|
||||
min-width: 2em;
|
||||
text-align: right;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
div.header-description {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
div.panel-header:hover {
|
||||
cursor: pointer;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
div.panel-header.active {
|
||||
background-color: #efefef;
|
||||
text-shadow: 1px 1px 0px rgba(255,255,255,0.75);
|
||||
}
|
||||
|
||||
div.panel-content {
|
||||
width: 100%;
|
||||
clear: both;
|
||||
//border: 1px solid #ccc;
|
||||
margin-bottom: 0.5em;
|
||||
background-color: #f9f9f9;
|
||||
padding: 1.5em;
|
||||
|
||||
div.table-wrap {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
border-right: 1px solid #ceede3;
|
||||
max-height: 23em;
|
||||
}
|
||||
|
||||
table {
|
||||
background-color: white;
|
||||
margin-bottom: 0;
|
||||
td, th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
tr.error {
|
||||
//background-color: #ffe6e4;
|
||||
//color: #ee4728;
|
||||
color: #c84C4c;
|
||||
}
|
||||
tr:hover td.invalid {
|
||||
background-color: #ed5135;
|
||||
}
|
||||
tr i {
|
||||
display: block;
|
||||
margin-bottom: -0.2em;
|
||||
font-size: 1.4em !important;
|
||||
}
|
||||
td.invalid {
|
||||
background-color: #f05c51;
|
||||
box-shadow: inset 0px 0px 1px red;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
div.import-errors {
|
||||
margin-bottom: 0.85em;
|
||||
|
||||
p.line {
|
||||
font-size: 1.15em;
|
||||
margin-bottom: 0.2em;
|
||||
color: #577084;
|
||||
}
|
||||
p.error {
|
||||
color: #cb1b1b;
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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-description {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
span.header-error {
|
||||
font-size: 0.85em;
|
||||
color: #da5354;
|
||||
}
|
||||
|
||||
.select2-search {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.select2-results {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.post-save-results {
|
||||
p {
|
||||
font-size: 1.25em;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
strong {
|
||||
margin-right: 0.2em;
|
||||
min-width: 1.8em;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 1.4em;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
i.fa-check-circle {
|
||||
color: #86d83a;
|
||||
}
|
||||
i.fa-info-circle {
|
||||
color: #68b7c0;
|
||||
}
|
||||
}
|
||||
|
||||
p.save-error {
|
||||
color: #ee4728;
|
||||
font-size: 1.05em;
|
||||
margin-top: 0.4em;
|
||||
}
|
||||
}
|
||||
62
app/controllers/admin/product_import_controller.rb
Normal file
62
app/controllers/admin/product_import_controller.rb
Normal file
@@ -0,0 +1,62 @@
|
||||
require 'roo'
|
||||
|
||||
class Admin::ProductImportController < Spree::Admin::BaseController
|
||||
|
||||
before_filter :validate_upload_presence, except: :index
|
||||
|
||||
def import
|
||||
# Save uploaded file to tmp directory
|
||||
@filepath = save_uploaded_file(params[:file])
|
||||
@importer = ProductImporter.new(File.new(@filepath), editable_enterprises)
|
||||
|
||||
check_file_errors @importer
|
||||
check_spreadsheet_has_data @importer
|
||||
|
||||
@tax_categories = Spree::TaxCategory.order('is_default DESC, name ASC')
|
||||
@shipping_categories = Spree::ShippingCategory.order('name ASC')
|
||||
end
|
||||
|
||||
def save
|
||||
@importer = ProductImporter.new(File.new(params[:filepath]), editable_enterprises, params[:settings])
|
||||
@importer.save_all if @importer.has_valid_entries?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_upload_presence
|
||||
unless params[:file] || (params[:filepath] && File.exist?(params[:filepath]))
|
||||
redirect_to '/admin/product_import', notice: 'File not found or could not be opened'
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
def check_file_errors(importer)
|
||||
if importer.errors.present?
|
||||
redirect_to '/admin/product_import', notice: @importer.errors.full_messages.to_sentence
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
def check_spreadsheet_has_data(importer)
|
||||
unless importer.item_count
|
||||
redirect_to '/admin/product_import', notice: 'No data found in spreadsheet'
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
def save_uploaded_file(upload)
|
||||
filename = 'import' + Time.now.strftime('%d-%m-%Y-%H-%M-%S')
|
||||
extension = '.' + upload.original_filename.split('.').last
|
||||
directory = 'tmp/product_import'
|
||||
Dir.mkdir(directory) unless File.exists?(directory)
|
||||
File.open(Rails.root.join(directory, filename+extension), 'wb') do |f|
|
||||
f.write(upload.read)
|
||||
f.path
|
||||
end
|
||||
end
|
||||
|
||||
# Define custom model class for Cancan permissions
|
||||
def model_class
|
||||
ProductImporter
|
||||
end
|
||||
end
|
||||
464
app/models/product_importer.rb
Normal file
464
app/models/product_importer.rb
Normal file
@@ -0,0 +1,464 @@
|
||||
require 'roo'
|
||||
|
||||
class ProductImporter
|
||||
extend ActiveModel::Naming
|
||||
include ActiveModel::Conversion
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_reader :total_supplier_products
|
||||
|
||||
def initialize(file, editable_enterprises, import_settings={})
|
||||
if file.is_a?(File)
|
||||
@file = file
|
||||
@sheet = open_spreadsheet
|
||||
@entries = []
|
||||
@valid_entries = {}
|
||||
@invalid_entries = {}
|
||||
|
||||
@products_to_create = {}
|
||||
@variants_to_create = {}
|
||||
@variants_to_update = {}
|
||||
|
||||
@products_created = 0
|
||||
@variants_created = 0
|
||||
@variants_updated = 0
|
||||
|
||||
@import_settings = import_settings
|
||||
@editable_enterprises = {}
|
||||
editable_enterprises.map { |e| @editable_enterprises[e.name] = e.id }
|
||||
|
||||
@total_supplier_products = 0
|
||||
@products_to_reset = {}
|
||||
@updated_ids = []
|
||||
|
||||
init_product_importer if @sheet
|
||||
else
|
||||
self.errors.add(:importer, 'error: no file uploaded')
|
||||
end
|
||||
end
|
||||
|
||||
def persisted?
|
||||
false #ActiveModel, not ActiveRecord
|
||||
end
|
||||
|
||||
def has_valid_entries?
|
||||
valid_count and valid_count > 0
|
||||
end
|
||||
|
||||
def item_count
|
||||
@sheet ? @sheet.last_row - 1 : 0
|
||||
end
|
||||
|
||||
def products_to_reset
|
||||
# Return indexed data about existing product count, reset count, and updates count per supplier
|
||||
@products_to_reset.each do |supplier_id, values|
|
||||
values[:updates_count] = 0 if values[:updates_count].blank?
|
||||
|
||||
if values[:updates_count] and values[:existing_products]
|
||||
@products_to_reset[supplier_id][:reset_count] = values[:existing_products] - values[:updates_count]
|
||||
end
|
||||
end
|
||||
@products_to_reset
|
||||
end
|
||||
|
||||
def valid_count
|
||||
@valid_entries.count
|
||||
end
|
||||
|
||||
def invalid_count
|
||||
@invalid_entries.count
|
||||
end
|
||||
|
||||
def products_create_count
|
||||
@products_to_create.count + @variants_to_create.count
|
||||
end
|
||||
|
||||
def products_update_count
|
||||
@variants_to_update.count
|
||||
end
|
||||
|
||||
def suppliers_index
|
||||
index = @suppliers_index || build_suppliers_index
|
||||
index.sort_by{ |k,v| v.to_i }.reverse.to_h
|
||||
end
|
||||
|
||||
def all_entries
|
||||
invalid_entries.merge(products_to_create).merge(products_to_update).sort.to_h
|
||||
end
|
||||
|
||||
def invalid_entries
|
||||
@invalid_entries
|
||||
end
|
||||
|
||||
def products_to_create
|
||||
@products_to_create.merge(@variants_to_create)
|
||||
end
|
||||
|
||||
def products_to_update
|
||||
@variants_to_update
|
||||
end
|
||||
|
||||
def products_created_count
|
||||
@products_created + @variants_created
|
||||
end
|
||||
|
||||
def products_updated_count
|
||||
@variants_updated
|
||||
end
|
||||
|
||||
def products_reset_count
|
||||
@products_reset_count || 0
|
||||
end
|
||||
|
||||
def total_saved_count
|
||||
@products_created + @variants_created + @variants_updated
|
||||
end
|
||||
|
||||
def save_all
|
||||
save_all_valid
|
||||
delete_uploaded_file
|
||||
end
|
||||
|
||||
def permission_by_name?(supplier_name)
|
||||
@editable_enterprises.has_key?(supplier_name)
|
||||
end
|
||||
|
||||
def permission_by_id?(supplier_id)
|
||||
@editable_enterprises.has_value?(Integer(supplier_id))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init_product_importer
|
||||
build_entries
|
||||
build_categories_index
|
||||
build_suppliers_index
|
||||
validate_all
|
||||
end
|
||||
|
||||
def open_spreadsheet
|
||||
if accepted_mimetype
|
||||
Roo::Spreadsheet.open(@file, extension: accepted_mimetype)
|
||||
else
|
||||
self.errors.add(:importer, 'could not process file: invalid filetype')
|
||||
delete_uploaded_file
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def accepted_mimetype
|
||||
File.extname(@file.path).in?('.csv', '.xls', '.xlsx', '.ods') ? @file.path.split('.').last.to_sym : false
|
||||
end
|
||||
|
||||
def headers
|
||||
@sheet.row(1)
|
||||
end
|
||||
|
||||
def rows
|
||||
return [] unless @sheet and @sheet.last_row
|
||||
(2..@sheet.last_row).map do |i|
|
||||
@sheet.row(i)
|
||||
end
|
||||
end
|
||||
|
||||
def build_entries
|
||||
rows.each_with_index do |row, i|
|
||||
row_data = Hash[[headers, row].transpose]
|
||||
entry = SpreadsheetEntry.new(row_data)
|
||||
entry.line_number = i+2
|
||||
@entries.push entry
|
||||
end
|
||||
@entries
|
||||
end
|
||||
|
||||
def validate_all
|
||||
@entries.each do |entry|
|
||||
supplier_validation(entry)
|
||||
category_validation(entry)
|
||||
set_update_status(entry)
|
||||
|
||||
mark_as_valid(entry) unless entry_invalid?(entry.line_number)
|
||||
end
|
||||
|
||||
count_existing_products
|
||||
delete_uploaded_file if item_count.zero? or valid_count.zero?
|
||||
end
|
||||
|
||||
def count_existing_products
|
||||
@suppliers_index.each do |supplier_name, supplier_id|
|
||||
if supplier_id and permission_by_id?(supplier_id)
|
||||
products_count = Spree::Variant.joins(:product).
|
||||
where('spree_products.supplier_id IN (?)
|
||||
AND spree_variants.is_master = false
|
||||
AND spree_variants.deleted_at IS NULL', supplier_id).
|
||||
count
|
||||
|
||||
if @products_to_reset[supplier_id]
|
||||
@products_to_reset[supplier_id][:existing_products] = products_count
|
||||
else
|
||||
@products_to_reset[supplier_id] = {existing_products: products_count}
|
||||
end
|
||||
|
||||
@total_supplier_products += products_count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def entry_invalid?(line_number)
|
||||
!!@invalid_entries[line_number]
|
||||
end
|
||||
|
||||
def supplier_validation(entry)
|
||||
supplier_name = entry.supplier
|
||||
|
||||
if supplier_name.blank?
|
||||
mark_as_invalid(entry, attribute: "supplier", error: "can't be blank")
|
||||
return
|
||||
end
|
||||
|
||||
unless supplier_exists?(supplier_name)
|
||||
mark_as_invalid(entry, attribute: "supplier", error: "\"#{supplier_name}\" not found in database")
|
||||
return
|
||||
end
|
||||
|
||||
unless permission_by_name?(supplier_name)
|
||||
mark_as_invalid(entry, attribute: "supplier", error: "\"#{supplier_name}\": you do not have permission to manage products for this enterprise")
|
||||
return
|
||||
end
|
||||
|
||||
entry.supplier_id = @suppliers_index[supplier_name]
|
||||
end
|
||||
|
||||
def supplier_exists?(supplier_name)
|
||||
@suppliers_index[supplier_name]
|
||||
end
|
||||
|
||||
def category_validation(entry)
|
||||
category_name = entry.category
|
||||
|
||||
if category_name.blank?
|
||||
mark_as_invalid(entry, attribute: "category", error: "can't be blank")
|
||||
return
|
||||
end
|
||||
|
||||
if category_exists?(category_name)
|
||||
entry.primary_taxon_id = @categories_index[category_name]
|
||||
else
|
||||
mark_as_invalid(entry, attribute: "category", error: "\"#{category_name}\" not found in database")
|
||||
end
|
||||
end
|
||||
|
||||
def category_exists?(category_name)
|
||||
@categories_index[category_name]
|
||||
end
|
||||
|
||||
def mark_as_valid(entry)
|
||||
@valid_entries[entry.line_number] = entry
|
||||
end
|
||||
|
||||
def mark_as_invalid(entry, options={})
|
||||
entry.errors.add(options[:attribute], options[:error]) if options[:attribute] and options[:error]
|
||||
entry.product_validations = options[:product_validations] if options[:product_validations]
|
||||
|
||||
@invalid_entries[entry.line_number] = entry
|
||||
end
|
||||
|
||||
# Minimise db queries by getting a list of suppliers to look
|
||||
# up, instead of doing a query for each entry in the spreadsheet
|
||||
def build_suppliers_index
|
||||
@suppliers_index = {}
|
||||
@entries.each do |entry|
|
||||
supplier_name = entry.supplier
|
||||
supplier_id = @suppliers_index[supplier_name] ||
|
||||
Enterprise.find_by_name(supplier_name, :select => 'id, name').try(:id)
|
||||
@suppliers_index[supplier_name] = supplier_id
|
||||
end
|
||||
@suppliers_index
|
||||
end
|
||||
|
||||
def build_categories_index
|
||||
@categories_index = {}
|
||||
@entries.each do |entry|
|
||||
category_name = entry.category
|
||||
category_id = @categories_index[category_name] ||
|
||||
Spree::Taxon.find_by_name(category_name, :select => 'id, name').try(:id)
|
||||
@categories_index[category_name] = category_id
|
||||
end
|
||||
@categories_index
|
||||
end
|
||||
|
||||
def save_all_valid
|
||||
already_created = {}
|
||||
@products_to_create.each do |line_number, entry|
|
||||
# If we've already added a new product with these attributes
|
||||
# from this spreadsheet, mark this entry as a new variant with
|
||||
# the new product id, as this is a now variant of that product...
|
||||
if already_created[entry.supplier_id] and already_created[entry.supplier_id][entry.name]
|
||||
product_id = already_created[entry.supplier_id][entry.name]
|
||||
mark_as_new_variant(entry, product_id)
|
||||
next
|
||||
end
|
||||
|
||||
product = Spree::Product.new()
|
||||
product.assign_attributes(entry.attributes.except('id'))
|
||||
assign_defaults(product, entry.attributes)
|
||||
if product.save
|
||||
ensure_variant_updated(product, entry)
|
||||
@products_created += 1
|
||||
@updated_ids.push product.variants.first.id
|
||||
else
|
||||
self.errors.add("Line #{line_number}:", product.errors.full_messages) #TODO: change
|
||||
end
|
||||
|
||||
already_created[entry.supplier_id] = {entry.name => product.id}
|
||||
end
|
||||
|
||||
@variants_to_update.each do |line_number, entry|
|
||||
variant = entry.product_object
|
||||
assign_defaults(variant, entry.attributes)
|
||||
if variant.valid? and variant.save
|
||||
@variants_updated += 1
|
||||
@updated_ids.push variant.id
|
||||
else
|
||||
self.errors.add("Line #{line_number}:", variant.errors.full_messages) #TODO: change
|
||||
end
|
||||
end
|
||||
|
||||
@variants_to_create.each do |line_number, entry|
|
||||
new_variant = entry.product_object
|
||||
assign_defaults(new_variant, entry.attributes)
|
||||
if new_variant.valid? and new_variant.save
|
||||
@variants_created += 1
|
||||
@updated_ids.push new_variant.id
|
||||
else
|
||||
self.errors.add("Line #{line_number}:", new_variant.errors.full_messages)
|
||||
end
|
||||
end
|
||||
|
||||
self.errors.add(:importer, "did not save any products successfully") if total_saved_count.zero?
|
||||
|
||||
reset_absent_products
|
||||
total_saved_count
|
||||
end
|
||||
|
||||
def reset_absent_products
|
||||
return if total_saved_count.zero?
|
||||
|
||||
enterprises_to_reset = []
|
||||
@import_settings.each do |enterprise_id, settings|
|
||||
enterprises_to_reset.push enterprise_id if settings['reset_all_absent'] and permission_by_id?(enterprise_id)
|
||||
end
|
||||
|
||||
unless enterprises_to_reset.empty? or @updated_ids.empty?
|
||||
# For selected enterprises; set stock to zero for all products
|
||||
# that were not present in the uploaded spreadsheet
|
||||
@products_reset_count = Spree::Variant.joins(:product).
|
||||
where('spree_products.supplier_id IN (?)
|
||||
AND spree_variants.id NOT IN (?)
|
||||
AND spree_variants.is_master = false
|
||||
AND spree_variants.deleted_at IS NULL', enterprises_to_reset, @updated_ids).
|
||||
update_all(count_on_hand: 0)
|
||||
end
|
||||
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(product, entry)
|
||||
# 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(entry)
|
||||
# Find product with matching supplier and name
|
||||
match = Spree::Product.where(supplier_id: entry.supplier_id, name: entry.name, deleted_at: nil).first
|
||||
|
||||
# If no matching product was found, create a new product
|
||||
if match.nil?
|
||||
mark_as_new_product(entry)
|
||||
return
|
||||
end
|
||||
|
||||
# Otherwise, if a variant exists with matching display_name and unit_value, update it
|
||||
match.variants.each do |existing_variant|
|
||||
if existing_variant.display_name == entry.display_name && existing_variant.unit_value == Float(entry.unit_value)
|
||||
mark_as_existing_variant(entry, existing_variant)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
# Otherwise, a variant with sufficiently matching attributes doesn't exist; create a new one
|
||||
mark_as_new_variant(entry, match.id)
|
||||
end
|
||||
|
||||
def mark_as_new_product(entry)
|
||||
new_product = Spree::Product.new()
|
||||
new_product.assign_attributes(entry.attributes.except('id'))
|
||||
if new_product.valid?
|
||||
@products_to_create[entry.line_number] = entry unless entry_invalid?(entry.line_number)
|
||||
else
|
||||
mark_as_invalid(entry, product_validations: new_product.errors)
|
||||
end
|
||||
end
|
||||
|
||||
def mark_as_existing_variant(entry, existing_variant)
|
||||
existing_variant.assign_attributes(entry.attributes.except('id', 'product_id'))
|
||||
check_on_hand_nil(entry, existing_variant)
|
||||
if existing_variant.valid?
|
||||
entry.product_object = existing_variant
|
||||
@variants_to_update[entry.line_number] = entry unless entry_invalid?(entry.line_number)
|
||||
updates_count_per_supplier(entry.supplier_id) unless entry_invalid?(entry.line_number)
|
||||
else
|
||||
mark_as_invalid(entry, product_validations: existing_variant.errors)
|
||||
end
|
||||
end
|
||||
|
||||
def mark_as_new_variant(entry, product_id)
|
||||
new_variant = Spree::Variant.new(entry.attributes.except('id', 'product_id'))
|
||||
new_variant.product_id = product_id
|
||||
check_on_hand_nil(entry, new_variant)
|
||||
if new_variant.valid?
|
||||
entry.product_object = new_variant
|
||||
@variants_to_create[entry.line_number] = entry unless entry_invalid?(entry.line_number)
|
||||
else
|
||||
mark_as_invalid(entry, product_validations: new_variant.errors)
|
||||
end
|
||||
end
|
||||
|
||||
def updates_count_per_supplier(supplier_id)
|
||||
if @products_to_reset[supplier_id] and @products_to_reset[supplier_id][:updates_count]
|
||||
@products_to_reset[supplier_id][:updates_count] += 1
|
||||
else
|
||||
@products_to_reset[supplier_id] = {updates_count: 1}
|
||||
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
|
||||
File.delete(@file)
|
||||
end
|
||||
end
|
||||
end
|
||||
68
app/models/spreadsheet_entry.rb
Normal file
68
app/models/spreadsheet_entry.rb
Normal file
@@ -0,0 +1,68 @@
|
||||
# Class for defining spreadsheet entry objects for use in ProductImporter
|
||||
class SpreadsheetEntry
|
||||
extend ActiveModel::Naming
|
||||
include ActiveModel::Conversion
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_accessor :line_number, :valid, :product_object, :product_validations, :save_type, :on_hand_nil
|
||||
|
||||
attr_accessor :id, :product_id, :supplier, :supplier_id, :name, :display_name, :sku,
|
||||
:unit_value, :unit_description, :variant_unit, :variant_unit_scale, :variant_unit_name,
|
||||
:display_as, :category, :primary_taxon_id, :price, :on_hand, :on_demand,
|
||||
:tax_category_id, :shipping_category_id, :description
|
||||
|
||||
def initialize(attrs)
|
||||
@product_validations = {}
|
||||
|
||||
attrs.each do |k, v|
|
||||
if self.respond_to?("#{k}=")
|
||||
send("#{k}=", v) unless non_product_attributes.include?(k)
|
||||
else
|
||||
# Trying to assign unknown attribute. Record this and give feedback or just ignore silently?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def persisted?
|
||||
false #ActiveModel
|
||||
end
|
||||
|
||||
def has_errors?
|
||||
self.errors.count > 0 or @product_validations.count > 0
|
||||
end
|
||||
|
||||
def attributes
|
||||
attrs = {}
|
||||
self.instance_variables.each do |var|
|
||||
attrs[var.to_s.delete("@")] = self.instance_variable_get(var)
|
||||
end
|
||||
attrs.except(*non_product_attributes)
|
||||
end
|
||||
|
||||
def displayable_attributes
|
||||
# Modified attributes list for displaying in user feedback
|
||||
attrs = {}
|
||||
self.instance_variables.each do |var|
|
||||
attrs[var.to_s.delete("@")] = self.instance_variable_get(var)
|
||||
end
|
||||
attrs.except(*non_product_attributes, *non_display_attributes)
|
||||
end
|
||||
|
||||
def invalid_attributes
|
||||
invalid_attrs = {}
|
||||
@product_validations.messages.merge(self.errors.messages).each do |attr, message|
|
||||
invalid_attrs[attr.to_s] = "#{attr.to_s.capitalize} #{message.first}"
|
||||
end
|
||||
invalid_attrs.except(*non_product_attributes, *non_display_attributes)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def non_display_attributes
|
||||
['id', 'product_id', 'variant_id', 'supplier_id', 'primary_taxon', 'primary_taxon_id', 'category_id', 'shipping_category_id', 'tax_category_id']
|
||||
end
|
||||
|
||||
def non_product_attributes
|
||||
['line_number', 'valid', 'errors', 'product_object', 'product_validations', 'save_type', 'on_hand_nil']
|
||||
end
|
||||
end
|
||||
@@ -155,6 +155,8 @@ class AbilityDecorator
|
||||
can [:admin, :index, :read, :search], Spree::Taxon
|
||||
can [:admin, :index, :read, :create, :edit], Spree::Classification
|
||||
|
||||
can [:admin, :index, :import, :save], 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
|
||||
end
|
||||
|
||||
@@ -16,7 +16,7 @@ module Spree
|
||||
adjustment.save
|
||||
else
|
||||
payment_method.create_adjustment(adjustment_label, order, self, true)
|
||||
reload
|
||||
association(:adjustment).reload
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ module Spree
|
||||
def adjust_with_included_tax(order)
|
||||
adjust_without_included_tax(order)
|
||||
|
||||
order.reload
|
||||
order.adjustments(:reload)
|
||||
order.line_items(:reload)
|
||||
(order.adjustments.tax + order.price_adjustments).each do |a|
|
||||
a.set_absolute_included_tax! a.amount
|
||||
end
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/ insert_bottom "[data-hook='admin_product_sub_tabs']"
|
||||
|
||||
-# Commenting out for now, until product import is finished
|
||||
-# = tab :product_import, label: "Import", url: main_app.admin_product_import_path, match_path: '/product_import'
|
||||
15
app/views/admin/product_import/_entries_table.html.haml
Normal file
15
app/views/admin/product_import/_entries_table.html.haml
Normal file
@@ -0,0 +1,15 @@
|
||||
- if entries && entries.count > 0
|
||||
%div.table-wrap
|
||||
%table
|
||||
%thead
|
||||
%th
|
||||
%th Line
|
||||
- entries.values.first.displayable_attributes.each do |key, value|
|
||||
%th= key
|
||||
- entries.each do |line_number, entry|
|
||||
%tr{class: ('error' if entry.has_errors?)}
|
||||
%td
|
||||
%i{class: (entry.has_errors? ? 'fa fa-warning warning' : 'fa fa-check-circle success')}
|
||||
%td= line_number
|
||||
- entry.displayable_attributes.each do |key, value|
|
||||
%td{class: ('invalid' if entry.has_errors? and entry.invalid_attributes[key])}= value
|
||||
11
app/views/admin/product_import/_errors_list.html.haml
Normal file
11
app/views/admin/product_import/_errors_list.html.haml
Normal file
@@ -0,0 +1,11 @@
|
||||
- @importer.invalid_entries.each do |line_number, entry|
|
||||
%div.import-errors
|
||||
%p.line
|
||||
%strong
|
||||
Item line #{line_number}:
|
||||
%span= entry.name
|
||||
- if entry.display_name
|
||||
( #{entry.display_name} )
|
||||
- entry.invalid_attributes.each do |attr, error|
|
||||
%p.error
|
||||
- #{error}
|
||||
49
app/views/admin/product_import/_import_options.html.haml
Normal file
49
app/views/admin/product_import/_import_options.html.haml
Normal file
@@ -0,0 +1,49 @@
|
||||
%h5 Import options and defaults
|
||||
%br
|
||||
|
||||
- @importer.suppliers_index.each do |name, supplier_id|
|
||||
- if name and supplier_id and @importer.permission_by_id?(supplier_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.neutral
|
||||
%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', supplier_id: supplier_id, name: name
|
||||
- elsif name and supplier_id
|
||||
%div.panel-section.import-settings{ng: {controller: 'DropdownPanelsCtrl'}}
|
||||
%div.panel-header
|
||||
%div.header-caret
|
||||
-#%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}"}}
|
||||
%div.header-icon.error
|
||||
%i.fa.fa-warning
|
||||
-#%div.header-count
|
||||
-# %strong.invalid-count= @importer.invalid_count
|
||||
%div.header-description
|
||||
= name
|
||||
%span.header-error= " - you do not have permission to manage this enterprise"
|
||||
-#%div.panel-content{ng: {hide: '!active'}}
|
||||
-# = render 'options_form', supplier_id: supplier_id, name: name
|
||||
- elsif name
|
||||
%div.panel-section.import-settings{ng: {controller: 'DropdownPanelsCtrl'}}
|
||||
%div.panel-header
|
||||
%div.header-caret
|
||||
-#%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}"}}
|
||||
%div.header-icon.error
|
||||
%i.fa.fa-warning
|
||||
-#%div.header-count
|
||||
-# %strong.invalid-count= @importer.invalid_count
|
||||
%div.header-description
|
||||
= name
|
||||
%span.header-error= " - enterprise could not be found in database"
|
||||
-#%div.panel-content{ng: {hide: '!active'}}
|
||||
-# = render 'options_form', supplier_id: supplier_id, name: name
|
||||
|
||||
|
||||
%br.panels.clearfix
|
||||
%br
|
||||
83
app/views/admin/product_import/_import_review.html.haml
Normal file
83
app/views/admin/product_import/_import_review.html.haml
Normal file
@@ -0,0 +1,83 @@
|
||||
%h5 Import validation overview
|
||||
%br
|
||||
|
||||
-#%div.panel-section
|
||||
-# %div.panel-header
|
||||
-# %div.header-caret
|
||||
-# %div.header-icon.info
|
||||
-# %i.fa.fa-info-circle
|
||||
-# %div.header-count
|
||||
-# %strong.existing-count= @importer.total_supplier_products
|
||||
-# %div.header-description
|
||||
-# Existing products in referenced enterprise(s)
|
||||
-# -#%div.panel-content{ng: {hide: '!active'}}
|
||||
-# -# Content goes here
|
||||
|
||||
%div.panel-section{ng: {controller: 'DropdownPanelsCtrl', init: "count = #{@importer.item_count}"}}
|
||||
%div.panel-header{ng: {click: 'togglePanel()', class: '{active: active && count}'}}
|
||||
%div.header-caret
|
||||
%i{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count == 0'}}
|
||||
%div.header-icon.success
|
||||
%i.fa.fa-info-circle.info
|
||||
%div.header-count
|
||||
%strong.item-count= @importer.item_count
|
||||
%div.header-description
|
||||
Entries found in imported file
|
||||
%div.panel-content{ng: {hide: '!active || count == 0'}}
|
||||
= render 'entries_table', entries: @importer.all_entries
|
||||
|
||||
%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{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count == 0'}}
|
||||
%div.header-icon.warning
|
||||
%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.panel-content{ng: {hide: '!active || count == 0'}}
|
||||
= render 'errors_list'
|
||||
%br
|
||||
= render 'entries_table', entries: @importer.invalid_entries
|
||||
|
||||
%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{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count == 0'}}
|
||||
%div.header-icon.success
|
||||
%i.fa.fa-check-circle
|
||||
%div.header-count
|
||||
%strong.create-count= @importer.products_create_count
|
||||
%div.header-description
|
||||
Products will be created
|
||||
%div.panel-content{ng: {hide: '!active || count == 0'}}
|
||||
= render 'entries_table', entries: @importer.products_to_create
|
||||
|
||||
%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{ng: {class: "{'icon-chevron-down': active, 'icon-chevron-right': !active}", hide: 'count == 0'}}
|
||||
%div.header-icon.success
|
||||
%i.fa.fa-check-circle
|
||||
%div.header-count
|
||||
%strong.update-count= @importer.products_update_count
|
||||
%div.header-description
|
||||
Products will be updated
|
||||
%div.panel-content{ng: {hide: '!active || count == 0'}}
|
||||
= render 'entries_table', entries: @importer.products_to_update
|
||||
|
||||
%div.panel-section{ng: {controller: 'ImportOptionsFormCtrl', hide: 'resetTotal == 0'}}
|
||||
%div.panel-header
|
||||
%div.header-caret
|
||||
%div.header-icon.info
|
||||
%i.fa.fa-info-circle
|
||||
%div.header-count
|
||||
%strong.reset-count
|
||||
{{resetTotal}}
|
||||
%div.header-description
|
||||
Existing products will have their stock reset to zero
|
||||
-#%div.panel-content{ng: {hide: '!active'}}
|
||||
-# Content goes here
|
||||
|
||||
%br.panels.clearfix
|
||||
35
app/views/admin/product_import/_options_form.html.haml
Normal file
35
app/views/admin/product_import/_options_form.html.haml
Normal file
@@ -0,0 +1,35 @@
|
||||
%table.import-settings{ng: {controller: 'ImportOptionsFormCtrl', init: "supplierId = #{supplier_id}; resetCount = #{@importer.products_to_reset[supplier_id][:reset_count]}"}}
|
||||
%tr
|
||||
%td.description
|
||||
Remove absent products?
|
||||
%td
|
||||
= check_box_tag "settings[#{supplier_id}][reset_all_absent]", 1, false, :'ng-model' => 'resetAbsent', :'ng-change' => 'toggleResetAbsent()'
|
||||
%td
|
||||
%tr
|
||||
%td.description
|
||||
Set default stock level
|
||||
%td
|
||||
= select_tag "settings[#{supplier_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[#{supplier_id}][defaults][on_hand][value]", 0
|
||||
%tr
|
||||
%td.description
|
||||
Set default tax category
|
||||
%td
|
||||
= select_tag "settings[#{supplier_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[#{supplier_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[#{supplier_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[#{supplier_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[#{supplier_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[#{supplier_id}][defaults][available_on][value]", nil, {class: 'datepicker', placeholder: 'Today'}
|
||||
9
app/views/admin/product_import/_upload_form.html.haml
Normal file
9
app/views/admin/product_import/_upload_form.html.haml
Normal file
@@ -0,0 +1,9 @@
|
||||
%h5 Select a spreadsheet to upload
|
||||
%br
|
||||
= form_tag main_app.admin_product_import_path, multipart: true do
|
||||
= file_field_tag :file
|
||||
%br
|
||||
%br
|
||||
= submit_tag "Import"
|
||||
%br
|
||||
%br
|
||||
34
app/views/admin/product_import/import.html.haml
Normal file
34
app/views/admin/product_import/import.html.haml
Normal file
@@ -0,0 +1,34 @@
|
||||
- content_for :page_title do
|
||||
Product Import
|
||||
|
||||
= render partial: 'spree/admin/shared/product_sub_menu'
|
||||
|
||||
= form_tag main_app.admin_product_import_save_path, {'ng-app' => 'ofn.admin'} do
|
||||
|
||||
- if @importer.invalid_count && !@importer.has_valid_entries?
|
||||
%h5 No valid entries found
|
||||
%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?
|
||||
- if @importer.invalid_count > 0
|
||||
%br
|
||||
%h5 Imported file contains some invalid entries
|
||||
%p Save valid entries for now and discard the others?
|
||||
- else
|
||||
%h5 No errors detected!
|
||||
%p Save all imported products?
|
||||
%br
|
||||
= hidden_field_tag :filepath, @filepath
|
||||
= submit_tag "Save"
|
||||
%a.button{href: main_app.admin_product_import_path} Cancel
|
||||
|
||||
- else
|
||||
%br
|
||||
%a.button{href: main_app.admin_product_import_path} Cancel
|
||||
|
||||
|
||||
6
app/views/admin/product_import/index.html.haml
Normal file
6
app/views/admin/product_import/index.html.haml
Normal file
@@ -0,0 +1,6 @@
|
||||
- content_for :page_title do
|
||||
Product Import
|
||||
|
||||
= render :partial => 'spree/admin/shared/product_sub_menu'
|
||||
|
||||
= render 'upload_form'
|
||||
38
app/views/admin/product_import/save.html.haml
Normal file
38
app/views/admin/product_import/save.html.haml
Normal file
@@ -0,0 +1,38 @@
|
||||
- content_for :page_title do
|
||||
Product Import
|
||||
|
||||
= render :partial => 'spree/admin/shared/product_sub_menu'
|
||||
|
||||
%h5 Import final results
|
||||
%br
|
||||
|
||||
%div.post-save-results{ng: {app: 'ofn.admin'}}
|
||||
|
||||
%p
|
||||
%i.fa{ng: {class: "{'fa-info-circle': #{@importer.products_created_count} == 0, 'fa-check-circle': #{@importer.products_created_count} != 0}"}}
|
||||
%strong.created-count= @importer.products_created_count
|
||||
Products created
|
||||
|
||||
%p
|
||||
%i.fa{ng: {class: "{'fa-info-circle': #{@importer.products_updated_count} == 0, 'fa-check-circle': #{@importer.products_updated_count} != 0}"}}
|
||||
%strong.updated-count= @importer.products_updated_count
|
||||
Products updated
|
||||
|
||||
- if @importer.products_reset_count > 0
|
||||
%p
|
||||
%i.fa.fa-check-circle
|
||||
%strong.reset-count= @importer.products_reset_count
|
||||
Products had stock level reset to zero
|
||||
|
||||
%br
|
||||
|
||||
- if @importer.errors.count == 0
|
||||
%p All #{@importer.total_saved_count} items saved successfully
|
||||
- else
|
||||
%h5 Save errors
|
||||
- @importer.errors.full_messages.each do |error|
|
||||
%p.save-error
|
||||
- #{error}
|
||||
|
||||
%br
|
||||
%a.button{href: main_app.admin_product_import_path} Back
|
||||
@@ -4,30 +4,30 @@
|
||||
%legend
|
||||
= t :checkout_your_order
|
||||
%table
|
||||
%tr
|
||||
%tr.subtotal
|
||||
%th
|
||||
= t :checkout_cart_total
|
||||
%td.cart-total.text-right= display_checkout_subtotal(@order)
|
||||
|
||||
- checkout_adjustments_for(current_order, exclude: [:shipping, :payment, :line_item]).reject{ |a| a.amount == 0 }.each do |adjustment|
|
||||
%tr
|
||||
%tr.adjustment
|
||||
%th= adjustment.label
|
||||
%td.text-right= adjustment.display_amount.to_html
|
||||
|
||||
%tr
|
||||
%tr.shipping
|
||||
%th
|
||||
= t :checkout_shipping_price
|
||||
%td.shipping.text-right {{ Checkout.shippingPrice() | localizeCurrency }}
|
||||
%td.text-right {{ Checkout.shippingPrice() | localizeCurrency }}
|
||||
|
||||
%tr
|
||||
%tr.transaction-fee
|
||||
%th
|
||||
= t :payment_method_fee
|
||||
%td.text-right {{ Checkout.paymentPrice() | localizeCurrency }}
|
||||
|
||||
%tr
|
||||
%tr.total
|
||||
%th
|
||||
= t :checkout_total_price
|
||||
%td.total.text-right {{ Checkout.cartTotal() | localizeCurrency }}
|
||||
%td.text-right {{ Checkout.cartTotal() | localizeCurrency }}
|
||||
|
||||
//= f.submit "Purchase", class: "button", "ofn-focus" => "accordion['payment']"
|
||||
%a.button.secondary{href: cart_url}
|
||||
|
||||
@@ -114,6 +114,10 @@ Openfoodnetwork::Application.routes.draw do
|
||||
|
||||
get '/inventory', to: 'variant_overrides#index'
|
||||
|
||||
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'
|
||||
|
||||
resources :variant_overrides do
|
||||
post :bulk_update, on: :collection
|
||||
post :bulk_reset, on: :collection
|
||||
@@ -237,7 +241,6 @@ Spree::Core::Engine.routes.prepend do
|
||||
|
||||
namespace :admin do
|
||||
get '/search/known_users' => "search#known_users", :as => :search_known_users
|
||||
|
||||
get '/search/customers' => 'search#customers', :as => :search_customers
|
||||
|
||||
resources :products do
|
||||
|
||||
@@ -94,6 +94,7 @@ feature %q{
|
||||
|
||||
within (".side_menu") { click_link "Users" }
|
||||
select2_search user.email, from: 'Owner'
|
||||
expect(page).to have_no_selector '.select2-drop-mask' # Ensure select2 has finished
|
||||
|
||||
click_link "About"
|
||||
fill_in 'enterprise_description', :with => 'Connecting farmers and eaters'
|
||||
|
||||
343
spec/features/admin/product_import_spec.rb
Normal file
343
spec/features/admin/product_import_spec.rb
Normal file
@@ -0,0 +1,343 @@
|
||||
require 'spec_helper'
|
||||
require 'open_food_network/permissions'
|
||||
|
||||
feature "Product Import", js: true do
|
||||
include AuthenticationWorkflow
|
||||
include WebHelper
|
||||
|
||||
let!(:admin) { create(:admin_user) }
|
||||
let!(:user) { create_enterprise_user }
|
||||
let!(:enterprise) { create(:supplier_enterprise, owner: user, name: "User Enterprise") }
|
||||
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: 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') }
|
||||
let!(:product4) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Cabbage') }
|
||||
let!(:product5) { create(:simple_product, supplier: enterprise2, on_hand: '100', name: 'Lettuce') }
|
||||
|
||||
|
||||
describe "when importing products from uploaded file" do
|
||||
before { quick_login_as_admin }
|
||||
after { File.delete('/tmp/test.csv') }
|
||||
|
||||
it "validates entries and saves them if they are all valid" 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)
|
||||
|
||||
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: "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_selector '.created-count', text: '2'
|
||||
expect(page).to have_selector '.updated-count', text: '0'
|
||||
|
||||
potatoes = Spree::Product.find_by_name('Potatoes')
|
||||
potatoes.supplier.should == enterprise
|
||||
potatoes.on_hand.should == 6
|
||||
potatoes.price.should == 6.50
|
||||
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 '.update-count', text: "0"
|
||||
|
||||
expect(page).to have_selector 'input[type=submit][value="Save"]'
|
||||
click_button 'Save'
|
||||
|
||||
expect(page).to have_selector '.created-count', text: '1'
|
||||
expect(page).to have_selector '.updated-count', text: '0'
|
||||
|
||||
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"]
|
||||
csv << ["Bad Carrots", "Unkown Enterprise", "Mouldy vegetables", "666", "3.20", "", "weight", ""]
|
||||
csv << ["Bad Potatoes", "", "Vegetables", "6", "6", "6", "", "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: "2"
|
||||
expect(page).to have_selector '.create-count', text: "0"
|
||||
expect(page).to have_selector '.update-count', text: "0"
|
||||
|
||||
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 have_selector '.invalid-count', text: "0"
|
||||
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
|
||||
|
||||
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
|
||||
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_selector '.created-count', text: '2'
|
||||
expect(page).to have_selector '.updated-count', text: '0'
|
||||
|
||||
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
|
||||
before { quick_login_as_admin }
|
||||
|
||||
it "checks filetype on upload" do
|
||||
File.write('/tmp/test.txt', "Wrong filetype!")
|
||||
|
||||
visit main_app.admin_product_import_path
|
||||
attach_file 'file', '/tmp/test.txt'
|
||||
click_button 'Import'
|
||||
|
||||
expect(page).to have_content "Importer could not process file: invalid filetype"
|
||||
expect(page).to_not have_selector 'input[type=submit][value="Save"]'
|
||||
expect(page).to have_content "Select a spreadsheet to upload"
|
||||
File.delete('/tmp/test.txt')
|
||||
end
|
||||
|
||||
it "returns and error if nothing was uploaded" do
|
||||
visit main_app.admin_product_import_path
|
||||
click_button 'Import'
|
||||
|
||||
expect(page).to have_content "File not found or could not be opened"
|
||||
end
|
||||
|
||||
it "handles cases where no meaningful data can be read from the file" do
|
||||
File.write('/tmp/test.csv', "A22££S\\\\\n**VA,,,AF..D")
|
||||
|
||||
visit main_app.admin_product_import_path
|
||||
attach_file 'file', '/tmp/test.csv'
|
||||
click_button 'Import'
|
||||
|
||||
expect(page).to have_selector '.create-count', text: "0"
|
||||
expect(page).to have_selector '.update-count', text: "0"
|
||||
expect(page).to_not have_selector 'input[type=submit][value="Save"]'
|
||||
File.delete('/tmp/test.csv')
|
||||
end
|
||||
end
|
||||
|
||||
describe "handling enterprise permissions" do
|
||||
before { quick_login_as user }
|
||||
|
||||
it "only allows 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.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: "1"
|
||||
expect(page).to have_selector '.create-count', text: "1"
|
||||
expect(page).to have_selector '.update-count', text: "0"
|
||||
|
||||
expect(page.body).to have_content 'you do not have permission'
|
||||
|
||||
click_button 'Save'
|
||||
|
||||
expect(page).to have_selector '.created-count', text: '1'
|
||||
expect(page).to have_selector '.updated-count', text: '0'
|
||||
|
||||
Spree::Product.find_by_name('My Carrots').should be_a Spree::Product
|
||||
Spree::Product.find_by_name('Your Potatoes').should == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "applying settings and defaults on import" do
|
||||
before { quick_login_as_admin }
|
||||
|
||||
it "can set 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 have_selector '.invalid-count', text: "0"
|
||||
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 "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_selector '.created-count', text: '2'
|
||||
expect(page).to have_selector '.updated-count', text: '0'
|
||||
|
||||
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
|
||||
@@ -1,6 +1,5 @@
|
||||
require 'spec_helper'
|
||||
|
||||
|
||||
feature "As a consumer I want to check out my cart", js: true, retry: 3 do
|
||||
include AuthenticationWorkflow
|
||||
include ShopWorkflow
|
||||
@@ -31,7 +30,7 @@ feature "As a consumer I want to check out my cart", js: true, retry: 3 do
|
||||
let(:sm2) { create(:shipping_method, require_ship_address: false, name: "Donkeys", description: "blue", calculator: Spree::Calculator::FlatRate.new(preferred_amount: 4.56)) }
|
||||
let(:sm3) { create(:shipping_method, require_ship_address: false, name: "Local", tag_list: "local") }
|
||||
let!(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::PaymentMethod::Check") }
|
||||
let!(:pm2) { create(:payment_method, distributors: [distributor]) }
|
||||
let!(:pm2) { create(:payment_method, distributors: [distributor], calculator: Spree::Calculator::FlatRate.new(preferred_amount: 5.67)) }
|
||||
let!(:pm3) do
|
||||
Spree::Gateway::PayPalExpress.create!(name: "Paypal", environment: 'test', distributor_ids: [distributor.id]).tap do |pm|
|
||||
pm.preferred_login = 'devnull-facilitator_api1.rohanmitchell.com'
|
||||
@@ -40,6 +39,7 @@ feature "As a consumer I want to check out my cart", js: true, retry: 3 do
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
before do
|
||||
distributor.shipping_methods << sm1
|
||||
distributor.shipping_methods << sm2
|
||||
@@ -328,37 +328,63 @@ feature "As a consumer I want to check out my cart", js: true, retry: 3 do
|
||||
end
|
||||
end
|
||||
|
||||
context "with a credit card payment method" do
|
||||
let!(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: "Spree::Gateway::Bogus") }
|
||||
context "when we are charged a payment method fee (transaction fee)" do
|
||||
it "creates a payment including the transaction fee" do
|
||||
# Selecting the transaction fee, it is displayed
|
||||
expect(page).to have_selector ".transaction-fee td", text: "$0.00"
|
||||
expect(page).to have_selector ".total", text: "$11.23"
|
||||
|
||||
it "takes us to the order confirmation page when submitted with a valid credit card" do
|
||||
toggle_payment
|
||||
fill_in 'Card Number', with: "4111111111111111"
|
||||
select 'February', from: 'secrets.card_month'
|
||||
select (Date.current.year+1).to_s, from: 'secrets.card_year'
|
||||
fill_in 'Security Code', with: '123'
|
||||
choose "#{pm2.name} ($5.67)"
|
||||
|
||||
expect(page).to have_selector ".transaction-fee td", text: "$5.67"
|
||||
expect(page).to have_selector ".total", text: "$16.90"
|
||||
|
||||
place_order
|
||||
page.should have_content "Your order has been processed successfully"
|
||||
expect(page).to have_content "Your order has been processed successfully"
|
||||
|
||||
# Order should have a payment with the correct amount
|
||||
# There are two orders - our order and our new cart
|
||||
o = Spree::Order.complete.first
|
||||
o.payments.first.amount.should == 11.23
|
||||
expect(o.adjustments.payment_fee.first.amount).to eq 5.67
|
||||
expect(o.payments.first.amount).to eq(10 + 1.23 + 5.67) # items + fees + transaction
|
||||
end
|
||||
end
|
||||
|
||||
it "shows the payment processing failed message when submitted with an invalid credit card" do
|
||||
toggle_payment
|
||||
fill_in 'Card Number', with: "9999999988887777"
|
||||
select 'February', from: 'secrets.card_month'
|
||||
select (Date.current.year+1).to_s, from: 'secrets.card_year'
|
||||
fill_in 'Security Code', with: '123'
|
||||
describe "credit card payments" do
|
||||
["Spree::Gateway::Bogus", "Spree::Gateway::BogusSimple"].each do |gateway_type|
|
||||
context "with a credit card payment method using #{gateway_type}" do
|
||||
let!(:pm1) { create(:payment_method, distributors: [distributor], name: "Roger rabbit", type: gateway_type) }
|
||||
|
||||
place_order
|
||||
page.should have_content "Payment could not be processed, please check the details you entered"
|
||||
it "takes us to the order confirmation page when submitted with a valid credit card" do
|
||||
toggle_payment
|
||||
fill_in 'Card Number', with: "4111111111111111"
|
||||
select 'February', from: 'secrets.card_month'
|
||||
select (Date.current.year+1).to_s, from: 'secrets.card_year'
|
||||
fill_in 'Security Code', with: '123'
|
||||
|
||||
# Does not show duplicate shipping fee
|
||||
visit checkout_path
|
||||
page.should have_selector "th", text: "Shipping", count: 1
|
||||
place_order
|
||||
page.should have_content "Your order has been processed successfully"
|
||||
|
||||
# Order should have a payment with the correct amount
|
||||
o = Spree::Order.complete.first
|
||||
o.payments.first.amount.should == 11.23
|
||||
end
|
||||
|
||||
it "shows the payment processing failed message when submitted with an invalid credit card" do
|
||||
toggle_payment
|
||||
fill_in 'Card Number', with: "9999999988887777"
|
||||
select 'February', from: 'secrets.card_month'
|
||||
select (Date.current.year+1).to_s, from: 'secrets.card_year'
|
||||
fill_in 'Security Code', with: '123'
|
||||
|
||||
place_order
|
||||
page.should have_content "Payment could not be processed, please check the details you entered"
|
||||
|
||||
# Does not show duplicate shipping fee
|
||||
visit checkout_path
|
||||
page.should have_selector "th", text: "Shipping", count: 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
33
spec/models/product_importer_spec.rb
Normal file
33
spec/models/product_importer_spec.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
require 'spec_helper'
|
||||
require 'open_food_network/permissions'
|
||||
|
||||
describe ProductImporter do
|
||||
include AuthenticationWorkflow
|
||||
|
||||
let!(:admin) { create(:admin_user) }
|
||||
let!(:user) { create_enterprise_user }
|
||||
let!(:enterprise) { create(:enterprise, owner: user, name: "Test Enterprise") }
|
||||
let!(:category) { create(:taxon, name: 'Vegetables') }
|
||||
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
|
||||
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"]
|
||||
end
|
||||
File.write('/tmp/test-m.csv', csv_data)
|
||||
file = File.new('/tmp/test-m.csv')
|
||||
|
||||
importer = ProductImporter.new(file, permissions.editable_enterprises)
|
||||
|
||||
expect(importer.valid_count).to eq(2)
|
||||
expect(importer.invalid_count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
# Test handling of filetypes
|
||||
end
|
||||
Reference in New Issue
Block a user