Merge tag 'v1.8.8' into transifex

This commit is contained in:
Maikel Linke
2017-04-06 10:31:26 +10:00
28 changed files with 1586 additions and 33 deletions

View File

@@ -63,6 +63,7 @@ gem 'wkhtmltopdf-binary'
gem 'foreigner'
gem 'immigrant'
gem 'roo', '~> 2.7.0'
gem 'whenever', require: false

View File

@@ -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)

View File

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

View File

@@ -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

View File

@@ -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

View 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;
}
}

View 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

View 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

View 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

View File

@@ -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

View File

@@ -16,7 +16,7 @@ module Spree
adjustment.save
else
payment_method.create_adjustment(adjustment_label, order, self, true)
reload
association(:adjustment).reload
end
end

View File

@@ -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

View File

@@ -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'

View 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

View 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
&nbsp;-&nbsp; #{error}

View 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

View 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

View 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'}

View 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

View 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

View File

@@ -0,0 +1,6 @@
- content_for :page_title do
Product Import
= render :partial => 'spree/admin/shared/product_sub_menu'
= render 'upload_form'

View 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
&nbsp;-&nbsp; #{error}
%br
%a.button{href: main_app.admin_product_import_path} Back

View File

@@ -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}

View File

@@ -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

View File

@@ -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'

View 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

View File

@@ -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

View 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