mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-31 21:37:16 +00:00
PI reset absent products
This commit is contained in:
committed by
Rob Harrington
parent
14fb40a996
commit
24fcc3dd34
@@ -0,0 +1,9 @@
|
||||
angular.module("ofn.admin").controller "ImportOptionsFormCtrl", ($scope, $timeout, $rootScope, ProductImportService) ->
|
||||
|
||||
$scope.toggleResetAbsent = () ->
|
||||
ProductImportService.updateResetAbsent($scope.supplierId, $scope.nonUpdated, $scope.resetAbsent)
|
||||
|
||||
$scope.resetCount = ProductImportService.resetCount
|
||||
|
||||
$rootScope.$watch 'resetCount', (newValue) ->
|
||||
$scope.resetCount = newValue if newValue || newValue == 0
|
||||
@@ -0,0 +1,15 @@
|
||||
angular.module("ofn.admin").factory "ProductImportService", ($rootScope, $timeout) ->
|
||||
new class ProductImportService
|
||||
suppliers: {}
|
||||
resetCount: 0
|
||||
|
||||
updateResetAbsent: (supplierId, nonUpdated, resetAbsent) ->
|
||||
if resetAbsent
|
||||
@suppliers[supplierId] = nonUpdated
|
||||
@resetCount += nonUpdated
|
||||
else
|
||||
@suppliers[supplierId] = null
|
||||
@resetCount -= nonUpdated
|
||||
|
||||
$rootScope.resetCount = @resetCount
|
||||
|
||||
@@ -24,16 +24,23 @@ div.panel-section {
|
||||
text-align: center;
|
||||
padding-top: 0.18em;
|
||||
|
||||
.fa {
|
||||
i {
|
||||
font-size: 1.5em;
|
||||
line-height: 0.9em;
|
||||
}
|
||||
.fa-warning {
|
||||
color: #ee4728;
|
||||
}
|
||||
.fa-check-circle {
|
||||
color: #86d83a;
|
||||
}
|
||||
}
|
||||
|
||||
.neutral {
|
||||
color: #BFBFBF;
|
||||
}
|
||||
.warning {
|
||||
color: #ee4728;
|
||||
}
|
||||
.success {
|
||||
color: #86d83a;
|
||||
}
|
||||
.info {
|
||||
color: #68b7c0;
|
||||
}
|
||||
|
||||
div.header-count {
|
||||
@@ -145,9 +152,7 @@ table.import-settings {
|
||||
}
|
||||
|
||||
.panel-section.import-settings {
|
||||
.header-icon {
|
||||
color: #BFBFBF;
|
||||
}
|
||||
|
||||
.header-description {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ class ProductImporter
|
||||
editable_enterprises.map { |e| @editable_enterprises[e.name] = e.id }
|
||||
|
||||
@non_display_attributes = 'id', 'product_id', 'variant_id', 'supplier_id', 'primary_taxon_id', 'category_id', 'shipping_category_id', 'tax_category_id', 'on_hand_nil'
|
||||
@supplier_products = {total: 0, by_supplier: {}}
|
||||
@updated_ids = []
|
||||
|
||||
validate_all if @sheet
|
||||
else
|
||||
@@ -44,6 +46,18 @@ class ProductImporter
|
||||
@sheet ? @sheet.last_row - 1 : 0
|
||||
end
|
||||
|
||||
def supplier_products
|
||||
# Return indexed data about existing product count and update count per supplier
|
||||
@supplier_products[:by_supplier].each do |supplier_id, supplier_data|
|
||||
supplier_data[:updates_count] = 0 if supplier_data[:updates_count].blank?
|
||||
|
||||
if supplier_data[:updates_count] and supplier_data[:existing_products]
|
||||
@supplier_products[:by_supplier][supplier_id][:non_updated] = supplier_data[:existing_products] - supplier_data[:updates_count]
|
||||
end
|
||||
end
|
||||
@supplier_products
|
||||
end
|
||||
|
||||
def valid_count
|
||||
@valid_entries.count
|
||||
end
|
||||
@@ -140,7 +154,7 @@ class ProductImporter
|
||||
|
||||
def validate_all
|
||||
entries.each_with_index do |entry, i|
|
||||
line_number = i+2
|
||||
line_number = i+2 # Roo counts "line 2" as the first line of data
|
||||
|
||||
supplier_validation(line_number, entry)
|
||||
category_validation(line_number, entry)
|
||||
@@ -149,9 +163,26 @@ class ProductImporter
|
||||
mark_as_valid(line_number, entry) unless entry_invalid?(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
|
||||
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
|
||||
|
||||
@supplier_products[:by_supplier][supplier_id] = {existing_products: products_count}
|
||||
@supplier_products[:total] += products_count
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def entry_invalid?(line_number)
|
||||
!!@invalid_entries[line_number]
|
||||
end
|
||||
@@ -246,14 +277,14 @@ class ProductImporter
|
||||
end
|
||||
|
||||
def save_all_valid
|
||||
updated = {}
|
||||
already_created = {}
|
||||
@products_to_create.each do |line_number, data|
|
||||
entry = data[: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 updated[entry['supplier_id']] && updated[entry['supplier_id']][entry['name']]
|
||||
product_id = updated[entry['supplier_id']][entry['name']]
|
||||
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(line_number, entry, product_id)
|
||||
next
|
||||
end
|
||||
@@ -264,11 +295,12 @@ class ProductImporter
|
||||
if product.save
|
||||
ensure_variant_updated(entry, product)
|
||||
@products_created += 1
|
||||
@updated_ids.push product.variants.first.id
|
||||
else
|
||||
self.errors.add("Line #{line_number}:", product.errors.full_messages)
|
||||
end
|
||||
|
||||
updated[entry['supplier_id']] = {entry['name'] => product.id}
|
||||
already_created[entry['supplier_id']] = {entry['name'] => product.id}
|
||||
end
|
||||
|
||||
@variants_to_update.each do |line_number, data|
|
||||
@@ -276,6 +308,7 @@ class ProductImporter
|
||||
assign_defaults(variant, data[:entry])
|
||||
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)
|
||||
end
|
||||
@@ -286,16 +319,36 @@ class ProductImporter
|
||||
assign_defaults(new_variant, data[:entry])
|
||||
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 == 0
|
||||
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']
|
||||
end
|
||||
|
||||
unless enterprises_to_reset.empty? or @updated_ids.empty?
|
||||
# Set stock to zero for all products in selected enterprises that were not
|
||||
# present in the uploaded spreadsheet.
|
||||
Spree::Variant.joins(:product).
|
||||
where('spree_products.supplier_id IN (?)
|
||||
AND spree_variants.id NOT IN (?)', 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']
|
||||
@@ -356,6 +409,7 @@ class ProductImporter
|
||||
check_on_hand_nil(entry, existing_variant)
|
||||
if existing_variant.valid?
|
||||
@variants_to_update[line_number] = {entry: entry, variant: existing_variant} unless entry_invalid?(line_number)
|
||||
updates_count_per_supplier(entry['supplier_id']) unless entry_invalid?(line_number)
|
||||
else
|
||||
mark_as_invalid(line_number, entry, existing_variant.errors.full_messages)
|
||||
end
|
||||
@@ -372,6 +426,14 @@ class ProductImporter
|
||||
end
|
||||
end
|
||||
|
||||
def updates_count_per_supplier(supplier_id)
|
||||
if @supplier_products[:by_supplier][supplier_id] and @supplier_products[:by_supplier][supplier_id][:updates_count]
|
||||
@supplier_products[:by_supplier][supplier_id][:updates_count] += 1
|
||||
else
|
||||
@supplier_products[:by_supplier][supplier_id] = {updates_count: 1}
|
||||
end
|
||||
end
|
||||
|
||||
def check_on_hand_nil(entry, variant)
|
||||
if entry['on_hand'].blank?
|
||||
variant.on_hand = 0
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
%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
|
||||
%div.header-icon.neutral
|
||||
%i.fa.fa-edit
|
||||
-#%div.header-count
|
||||
-# %strong.invalid-count= @importer.invalid_count
|
||||
|
||||
@@ -4,9 +4,32 @@
|
||||
%div.panel-section
|
||||
%div.panel-header
|
||||
%div.header-caret
|
||||
-#%i.icon-chevron-right{ng: {hide: 'active'}}
|
||||
-#%i.icon-chevron-down{ng: {hide: '!active'}}
|
||||
%div.header-icon
|
||||
%div.header-icon.info
|
||||
%i.fa.fa-info-circle
|
||||
%div.header-count
|
||||
%strong.item-count= @importer.supplier_products[:total]
|
||||
%div.header-description
|
||||
Existing products in referenced enterprise(s)
|
||||
-#%div.panel-content{ng: {hide: '!active'}}
|
||||
-# Content goes here
|
||||
|
||||
%div.panel-section{ng: {controller: 'ImportOptionsFormCtrl', hide: 'resetCount == 0'}}
|
||||
%div.panel-header
|
||||
%div.header-caret
|
||||
%div.header-icon.info
|
||||
%i.fa.fa-info-circle
|
||||
%div.header-count
|
||||
%strong.reset-count
|
||||
{{resetCount}}
|
||||
%div.header-description
|
||||
Existing products will have their stock reset to zero
|
||||
-#%div.panel-content{ng: {hide: '!active'}}
|
||||
-# Content goes here
|
||||
|
||||
%div.panel-section
|
||||
%div.panel-header
|
||||
%div.header-caret
|
||||
%div.header-icon.success
|
||||
%i.fa.fa-check-circle
|
||||
%div.header-count
|
||||
%strong.item-count= @importer.item_count
|
||||
@@ -19,7 +42,7 @@
|
||||
%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
|
||||
%div.header-icon.warning
|
||||
%i.fa.fa-warning
|
||||
%div.header-count
|
||||
%strong.invalid-count= @importer.invalid_count
|
||||
@@ -34,7 +57,7 @@
|
||||
%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
|
||||
%div.header-icon.success
|
||||
%i.fa.fa-check-circle
|
||||
%div.header-count
|
||||
%strong.create-count= @importer.products_create_count
|
||||
@@ -47,7 +70,7 @@
|
||||
%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
|
||||
%div.header-icon.success
|
||||
%i.fa.fa-check-circle
|
||||
%div.header-count
|
||||
%strong.update-count= @importer.products_update_count
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
%table.import-settings
|
||||
%table.import-settings{ng: {controller: 'ImportOptionsFormCtrl', init: "supplierId = #{id}; nonUpdated = #{@importer.supplier_products[:by_supplier][id][:non_updated]}"}}
|
||||
%tr
|
||||
%td.description
|
||||
Remove absent products?
|
||||
%td
|
||||
= check_box_tag "settings[#{id}][products_absent]", 1, false
|
||||
= check_box_tag "settings[#{id}][reset_all_absent]", 1, false, :'ng-model' => 'resetAbsent', :'ng-change' => 'toggleResetAbsent()'
|
||||
%td
|
||||
%tr
|
||||
%td.description
|
||||
|
||||
@@ -14,8 +14,12 @@ feature "Product Import", js: true do
|
||||
let!(:tax_category) { create(:tax_category) }
|
||||
let!(:tax_category2) { create(:tax_category) }
|
||||
let!(:shipping_category) { create(:shipping_category) }
|
||||
let!(:product) { create(:simple_product, supplier: enterprise, name: 'Hypothetical Cake') }
|
||||
let!(:variant) { create(:variant, product_id: product.id, price: '8.50', count_on_hand: '100', unit_value: '500', display_name: 'Preexisting Banana') }
|
||||
let!(: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') }
|
||||
let!(:product3) { create(:simple_product, supplier: enterprise, on_hand: '100', name: 'Sprouts') }
|
||||
let!(:product4) { create(:simple_product, supplier: enterprise2, on_hand: '100', name: 'Lettuce') }
|
||||
|
||||
|
||||
describe "when importing products from uploaded file" do
|
||||
before { quick_login_as_admin }
|
||||
@@ -104,8 +108,8 @@ feature "Product Import", js: true do
|
||||
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", "User Enterprise", "Cake", "5", "5.50", "500", "weight", "1", "Preexisting Banana"]
|
||||
csv << ["Hypothetical Cake", "User Enterprise", "Cake", "6", "3.50", "500", "weight", "1", "Emergent Coffee"]
|
||||
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)
|
||||
|
||||
@@ -236,6 +240,36 @@ feature "Product Import", js: true do
|
||||
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 << ["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
|
||||
check "settings_#{enterprise.id}_reset_all_absent"
|
||||
end
|
||||
|
||||
expect(page).to have_selector '.reset-count', text: "2"
|
||||
|
||||
click_button 'Save'
|
||||
expect(page).to have_content "Products created: 2"
|
||||
|
||||
Spree::Product.find_by_name('Carrots').on_hand.should == 5 # Present in file
|
||||
Spree::Product.find_by_name('Potatoes').on_hand.should == 6 # Present in file
|
||||
Spree::Product.find_by_name('Beans').on_hand.should == 0 # In enterprise, not in file
|
||||
Spree::Product.find_by_name('Sprouts').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"]
|
||||
|
||||
Reference in New Issue
Block a user