diff --git a/app/assets/javascripts/admin/product_import/controllers/import_feedback.js.coffee b/app/assets/javascripts/admin/product_import/controllers/import_feedback.js.coffee index 990d3598bf..ea23c59d9f 100644 --- a/app/assets/javascripts/admin/product_import/controllers/import_feedback.js.coffee +++ b/app/assets/javascripts/admin/product_import/controllers/import_feedback.js.coffee @@ -8,3 +8,5 @@ angular.module("ofn.admin").controller "ImportFeedbackCtrl", ($scope) -> $scope.attribute_invalid = (attribute, line_number) -> $scope.entries[line_number]['errors'][attribute] != undefined + + $scope.ignore_fields = ['variant_unit', 'variant_unit_scale', 'unit_description'] diff --git a/app/models/product_importer.rb b/app/models/product_importer.rb index f782ab7ef1..7c2bf28208 100644 --- a/app/models/product_importer.rb +++ b/app/models/product_importer.rb @@ -241,7 +241,7 @@ class ProductImporter line_number = i + 1 row = @sheet.row(line_number) row_data = Hash[[headers, row].transpose] - entry = SpreadsheetEntry.new(row_data) + entry = SpreadsheetEntry.new(row_data, importing_into_inventory?) entry.line_number = line_number @entries.push entry return if @sheet.last_row == line_number # TODO: test @@ -251,7 +251,7 @@ class ProductImporter def build_entries rows.each_with_index do |row, i| row_data = Hash[[headers, row].transpose] - entry = SpreadsheetEntry.new(row_data) + entry = SpreadsheetEntry.new(row_data, importing_into_inventory?) entry.line_number = i + 2 @entries.push entry end @@ -289,6 +289,10 @@ class ProductImporter end match.variants.each do |existing_variant| + unit_scale = match.variant_unit_scale + unscaled_units = entry.unscaled_units || 0 + entry.unit_value = unscaled_units * unit_scale + if existing_variant.display_name == entry.display_name and existing_variant.unit_value == entry.unit_value.to_f variant_override = create_inventory_item(entry, existing_variant) validate_inventory_item(entry, variant_override) diff --git a/app/models/spreadsheet_entry.rb b/app/models/spreadsheet_entry.rb index a52d9501ca..ad7a496cea 100644 --- a/app/models/spreadsheet_entry.rb +++ b/app/models/spreadsheet_entry.rb @@ -7,17 +7,20 @@ class SpreadsheetEntry attr_reader :validates_as attr_accessor :line_number, :valid, :product_object, :product_validations, :on_hand_nil, - :has_overrides + :has_overrides, :units, :unscaled_units, :unit_type attr_accessor :id, :product_id, :producer, :producer_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, :count_on_hand, :on_demand, :tax_category_id, :shipping_category_id, :description, :import_date - def initialize(attrs) + def initialize(attrs, is_inventory=false) #@product_validations = {} @validates_as = '' + validate_custom_unit_fields(attrs, is_inventory) + convert_custom_unit_fields(attrs, is_inventory) + attrs.each do |k, v| if self.respond_to?("#{k}=") send("#{k}=", v) unless non_product_attributes.include?(k) @@ -28,6 +31,51 @@ class SpreadsheetEntry end end + def unit_scales + { + 'g' => {scale: 1, unit: 'weight'}, + 'kg' => {scale: 1000, unit: 'weight'}, + 't' => {scale: 1000000, unit: 'weight'}, + 'ml' => {scale: 0.001, unit: 'volume'}, + 'l' => {scale: 1, unit: 'volume'}, + 'kl' => {scale: 1000, unit: 'volume'} + } + end + + def convert_custom_unit_fields(attrs, is_inventory) + + # unit unit_type variant_unit_name -> unit_value variant_unit_scale variant_unit + # 250 ml nil .... 0.25 0.001 volume + # 50 g nil .... 50 1 weight + # 2 kg nil .... 2000 1000 weight + # 1 nil bunches .... 1 null items + + attrs['variant_unit'] = nil + attrs['variant_unit_scale'] = nil + attrs['unit_value'] = nil + + if is_inventory and attrs.has_key?('units') and attrs['units'].present? + attrs['unscaled_units'] = attrs['units'] + end + + if attrs.has_key?('units') and attrs.has_key?('unit_type') and attrs['units'].present? and attrs['unit_type'].present? + units = attrs['units'].to_f + unit_type = attrs['unit_type'].to_s.downcase + + if valid_unit_type? unit_type + attrs['variant_unit'] = unit_scales[unit_type][:unit] + attrs['variant_unit_scale'] = unit_scales[unit_type][:scale] + attrs['unit_value'] = (units || 0) * attrs['variant_unit_scale'] + end + end + + if attrs.has_key?('units') and attrs.has_key?('variant_unit_name') and attrs['units'].present? and attrs['variant_unit_name'].present? + attrs['variant_unit'] = 'items' + attrs['variant_unit_scale'] = nil + attrs['unit_value'] = units || 1 + end + end + def persisted? false #ActiveModel end @@ -74,8 +122,34 @@ class SpreadsheetEntry private + def valid_unit_type?(unit_type) + unit_scales.has_key? unit_type + end + + def validate_custom_unit_fields(attrs, is_inventory) + unit_types = ['g', 'kg', 't', 'ml', 'l', 'kl', ''] + + # unit must be present and not nil + unless attrs.has_key? 'units' and attrs['units'].present? + self.errors.add('units', "can't be blank") + end + + return if is_inventory + + # unit_type must be valid type + if attrs.has_key? 'unit_type' and attrs['unit_type'].present? + unit_type = attrs['unit_type'].to_s.strip.downcase + self.errors.add('unit_type', "incorrect value") unless unit_types.include?(unit_type) + end + + # variant_unit_name must be present if unit_type not present + if !attrs.has_key? 'unit_type' or ( attrs.has_key? 'unit_type' and attrs['unit_type'].blank? ) + self.errors.add('variant_unit_name', "can't be blank if unit_type is blank") unless attrs.has_key? 'variant_unit_name' and attrs['variant_unit_name'].present? + end + end + def non_display_attributes - ['id', 'product_id', 'variant_id', 'supplier_id', 'primary_taxon', 'primary_taxon_id', 'category_id', 'shipping_category_id', 'tax_category_id'] + ['id', 'product_id', 'unscaled_units', 'variant_id', 'supplier_id', 'primary_taxon', 'primary_taxon_id', 'category_id', 'shipping_category_id', 'tax_category_id'] end def non_product_attributes diff --git a/app/views/admin/product_import/_errors_list.html.haml b/app/views/admin/product_import/_errors_list.html.haml index 260ad46196..787aabe76b 100644 --- a/app/views/admin/product_import/_errors_list.html.haml +++ b/app/views/admin/product_import/_errors_list.html.haml @@ -5,5 +5,5 @@ %span {{entry.attributes.name}} %span{ng: {if: "entry.attributes.display_name"}} ( {{entry.attributes.display_name}} ) - %p.error{ng: {repeat: "(attribute, error) in entry.errors"}} + %p.error{ng: {repeat: "(attribute, error) in entry.errors", show: "ignore_fields.indexOf(attribute) < 0" }}  -  {{error}} diff --git a/spec/features/admin/product_import_spec.rb b/spec/features/admin/product_import_spec.rb index 8ec649f7e1..4c1396de70 100644 --- a/spec/features/admin/product_import_spec.rb +++ b/spec/features/admin/product_import_spec.rb @@ -33,9 +33,9 @@ feature "Product Import", js: true do it "validates entries and saves them if they are all valid and allows viewing new items in Bulk Products" do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"] - csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"] - csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1000", "weight", "1000"] + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type"] + csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g"] + csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1", "kg"] end File.write('/tmp/test.csv', csv_data) @@ -90,9 +90,9 @@ feature "Product Import", js: true do it "displays info about invalid entries but no save button if all items are invalid" do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"] - csv << ["Bad Carrots", "Unkown Enterprise", "Mouldy vegetables", "666", "3.20", "", "weight", ""] - csv << ["Bad Potatoes", "", "Vegetables", "6", "6", "6", "", "1000"] + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type"] + csv << ["Bad Carrots", "Unkown Enterprise", "Mouldy vegetables", "666", "3.20", "", "g"] + csv << ["Bad Potatoes", "", "Vegetables", "6", "6", "6", ""] end File.write('/tmp/test.csv', csv_data) @@ -116,9 +116,9 @@ feature "Product Import", js: true do it "records a timestamp on import that can be viewed and filtered under Bulk Edit Products" do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"] - csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"] - csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1000", "weight", "1000"] + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type"] + csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g"] + csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1", "kg"] end File.write('/tmp/test.csv', csv_data) @@ -169,7 +169,7 @@ feature "Product Import", js: true do it "can import items into inventory" do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "producer", "category", "on_hand", "price", "unit_value"] + csv << ["name", "supplier", "producer", "category", "on_hand", "price", "units"] csv << ["Beans", "Another Enterprise", "User Enterprise", "Vegetables", "5", "3.20", "500"] csv << ["Sprouts", "Another Enterprise", "User Enterprise", "Vegetables", "6", "6.50", "500"] csv << ["Cabbage", "Another Enterprise", "User Enterprise", "Vegetables", "2001", "1.50", "500"] @@ -272,9 +272,9 @@ feature "Product Import", js: true do it "only allows product import into enterprises the user is permitted to manage" do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"] - csv << ["My Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"] - csv << ["Your Potatoes", "Another Enterprise", "Vegetables", "6", "6.50", "1000", "weight", "1000"] + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type"] + csv << ["My Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g"] + csv << ["Your Potatoes", "Another Enterprise", "Vegetables", "6", "6.50", "1", "kg"] end File.write('/tmp/test.csv', csv_data) diff --git a/spec/models/product_importer_spec.rb b/spec/models/product_importer_spec.rb index 00f20dfe5f..08cb9d677c 100644 --- a/spec/models/product_importer_spec.rb +++ b/spec/models/product_importer_spec.rb @@ -31,9 +31,11 @@ describe ProductImporter do describe "importing products from a spreadsheet" do before do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"] - csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"] - csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1000", "weight", "1000"] + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type", "variant_unit_name"] + csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g", ""] + csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "2", "kg", ""] + csv << ["Pea Soup", "User Enterprise", "Vegetables", "8", "5.50", "750", "ml", ""] + csv << ["Salad", "User Enterprise", "Vegetables", "7", "4.50", "1", "", "bags"] end File.write('/tmp/test-m.csv', csv_data) file = File.new('/tmp/test-m.csv') @@ -42,46 +44,70 @@ describe ProductImporter do after { File.delete('/tmp/test-m.csv') } it "returns the number of entries" do - expect(@importer.item_count).to eq(2) + expect(@importer.item_count).to eq(4) end it "validates entries and returns the results as json" do @importer.validate_entries entries = JSON.parse(@importer.entries_json) - expect(filter('valid', entries)).to eq 2 + expect(filter('valid', entries)).to eq 4 expect(filter('invalid', entries)).to eq 0 - expect(filter('create_product', entries)).to eq 2 + expect(filter('create_product', entries)).to eq 4 expect(filter('update_product', entries)).to eq 0 end it "saves the results and returns info on updated products" do @importer.save_entries - expect(@importer.products_created_count).to eq 2 + expect(@importer.products_created_count).to eq 4 expect(@importer.updated_ids).to be_a(Array) - expect(@importer.updated_ids.count).to eq 2 + expect(@importer.updated_ids.count).to eq 4 carrots = Spree::Product.find_by_name('Carrots') carrots.supplier.should == enterprise carrots.on_hand.should == 5 carrots.price.should == 3.20 + carrots.unit_value.should == 500 + carrots.variant_unit.should == 'weight' + carrots.variant_unit_scale.should == 1 carrots.variants.first.import_date.should be_within(1.minute).of DateTime.now potatoes = Spree::Product.find_by_name('Potatoes') potatoes.supplier.should == enterprise potatoes.on_hand.should == 6 potatoes.price.should == 6.50 + potatoes.unit_value.should == 2000 + potatoes.variant_unit.should == 'weight' + potatoes.variant_unit_scale.should == 1000 potatoes.variants.first.import_date.should be_within(1.minute).of DateTime.now + + pea_soup = Spree::Product.find_by_name('Pea Soup') + pea_soup.supplier.should == enterprise + pea_soup.on_hand.should == 8 + pea_soup.price.should == 5.50 + pea_soup.unit_value.should == 0.75 + pea_soup.variant_unit.should == 'volume' + pea_soup.variant_unit_scale.should == 0.001 + pea_soup.variants.first.import_date.should be_within(1.minute).of DateTime.now + + salad = Spree::Product.find_by_name('Salad') + salad.supplier.should == enterprise + salad.on_hand.should == 7 + salad.price.should == 4.50 + salad.unit_value.should == 1 + salad.variant_unit.should == 'items' + salad.variant_unit_scale.should == nil + salad.variants.first.import_date.should be_within(1.minute).of DateTime.now end end describe "when uploading a spreadsheet with some invalid entries" do before do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"] - csv << ["Good Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"] - csv << ["Bad Potatoes", "", "Vegetables", "6", "6.50", "1000", "", "1000"] + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type"] + csv << ["Good Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g"] + csv << ["Bad Potatoes", "", "Vegetables", "6", "6.50", "1", ""] end File.write('/tmp/test-m.csv', csv_data) file = File.new('/tmp/test-m.csv') @@ -119,9 +145,9 @@ describe ProductImporter do describe "adding new variants to existing products and updating exiting products" do before do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale", "display_name"] - csv << ["Hypothetical Cake", "Another Enterprise", "Cake", "5", "5.50", "500", "weight", "1", "Preexisting Banana"] - csv << ["Hypothetical Cake", "Another Enterprise", "Cake", "6", "3.50", "500", "weight", "1", "Emergent Coffee"] + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type", "display_name"] + csv << ["Hypothetical Cake", "Another Enterprise", "Cake", "5", "5.50", "500", "g", "Preexisting Banana"] + csv << ["Hypothetical Cake", "Another Enterprise", "Cake", "6", "3.50", "500", "g", "Emergent Coffee"] end File.write('/tmp/test-m.csv', csv_data) file = File.new('/tmp/test-m.csv') @@ -165,9 +191,9 @@ describe ProductImporter do describe "adding new product and sub-variant at the same time" do before do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale", "display_name"] - csv << ["Potatoes", "User Enterprise", "Vegetables", "5", "3.50", "500", "weight", "1000", "Small Bag"] - csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "5.50", "2000", "weight", "1000", "Big Bag"] + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type", "display_name"] + csv << ["Potatoes", "User Enterprise", "Vegetables", "5", "3.50", "500", "g", "Small Bag"] + csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "5.50", "2", "kg", "Big Bag"] end File.write('/tmp/test-m.csv', csv_data) file = File.new('/tmp/test-m.csv') @@ -206,7 +232,7 @@ describe ProductImporter do describe "importing items into inventory" do before do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "producer", "category", "on_hand", "price", "unit_value"] + csv << ["name", "supplier", "producer", "category", "on_hand", "price", "units"] csv << ["Beans", "Another Enterprise", "User Enterprise", "Vegetables", "5", "3.20", "500"] csv << ["Sprouts", "Another Enterprise", "User Enterprise", "Vegetables", "6", "6.50", "500"] csv << ["Cabbage", "Another Enterprise", "User Enterprise", "Vegetables", "2001", "1.50", "500"] @@ -255,9 +281,9 @@ describe ProductImporter do it "only allows product import into enterprises the user is permitted to manage" do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"] - csv << ["My Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"] - csv << ["Your Potatoes", "Another Enterprise", "Vegetables", "6", "6.50", "1000", "weight", "1000"] + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type"] + csv << ["My Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g"] + csv << ["Your Potatoes", "Another Enterprise", "Vegetables", "6", "6.50", "1", "kg"] end File.write('/tmp/test-m.csv', csv_data) file = File.new('/tmp/test-m.csv') @@ -282,7 +308,7 @@ describe ProductImporter do it "allows creating inventories for producers that a user's hub has permission for" do csv_data = CSV.generate do |csv| - csv << ["name", "producer", "supplier", "category", "on_hand", "price", "unit_value"] + csv << ["name", "producer", "supplier", "category", "on_hand", "price", "units"] csv << ["Beans", "User Enterprise", "Another Enterprise", "Vegetables", "777", "3.20", "500"] end File.write('/tmp/test-m.csv', csv_data) @@ -308,7 +334,7 @@ describe ProductImporter do it "does not allow creating inventories for producers that a user's hubs don't have permission for" do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value"] + csv << ["name", "supplier", "category", "on_hand", "price", "units"] csv << ["Beans", "User Enterprise", "Vegetables", "5", "3.20", "500"] csv << ["Sprouts", "User Enterprise", "Vegetables", "6", "6.50", "500"] end @@ -336,9 +362,9 @@ describe ProductImporter do it "can reset all products for an enterprise that are not present in the uploaded file to zero stock" do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale"] - csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1"] - csv << ["Beans", "User Enterprise", "Vegetables", "6", "6.50", "500", "weight", "1"] + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type"] + csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g"] + csv << ["Beans", "User Enterprise", "Vegetables", "6", "6.50", "500", "g"] end File.write('/tmp/test-m.csv', csv_data) file = File.new('/tmp/test-m.csv') @@ -373,7 +399,7 @@ describe ProductImporter do it "can reset all inventory items for an enterprise that are not present in the uploaded file to zero stock" do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "producer", "category", "on_hand", "price", "unit_value"] + csv << ["name", "supplier", "producer", "category", "on_hand", "price", "units"] csv << ["Beans", "Another Enterprise", "User Enterprise", "Vegetables", "6", "3.20", "500"] csv << ["Sprouts", "Another Enterprise", "User Enterprise", "Vegetables", "7", "6.50", "500"] end @@ -411,9 +437,9 @@ describe ProductImporter do it "can overwrite fields with selected defaults when importing to product list" do csv_data = CSV.generate do |csv| - csv << ["name", "supplier", "category", "on_hand", "price", "unit_value", "variant_unit", "variant_unit_scale", "tax_category_id", "available_on"] - csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "weight", "1", tax_category.id, ""] - csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1000", "weight", "1000", "", ""] + csv << ["name", "supplier", "category", "on_hand", "price", "units", "unit_type", "tax_category_id", "available_on"] + csv << ["Carrots", "User Enterprise", "Vegetables", "5", "3.20", "500", "g", tax_category.id, ""] + csv << ["Potatoes", "User Enterprise", "Vegetables", "6", "6.50", "1", "kg", "", ""] end File.write('/tmp/test-m.csv', csv_data) file = File.new('/tmp/test-m.csv') @@ -473,7 +499,7 @@ describe ProductImporter do it "can overwrite fields with selected defaults when importing to inventory" do csv_data = CSV.generate do |csv| - csv << ["name", "producer", "supplier", "category", "on_hand", "price", "unit_value"] + csv << ["name", "producer", "supplier", "category", "on_hand", "price", "units"] csv << ["Beans", "User Enterprise", "Another Enterprise", "Vegetables", "", "3.20", "500"] csv << ["Sprouts", "User Enterprise", "Another Enterprise", "Vegetables", "7", "6.50", "500"] csv << ["Cabbage", "User Enterprise", "Another Enterprise", "Vegetables", "", "1.50", "500"] @@ -516,9 +542,7 @@ describe ProductImporter do sprouts_override.count_on_hand.should == 7 cabbage_override.count_on_hand.should == 9000 end - end - end private