PI human-readable unit fields

Enhanced unit specs
This commit is contained in:
Matt-Yorkley
2017-04-28 17:27:45 +01:00
parent 02661d5c23
commit fe01e8ede3
6 changed files with 156 additions and 52 deletions

View File

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

View File

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

View File

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

View File

@@ -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" }}
&nbsp;-&nbsp; {{error}}

View File

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

View File

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