mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-22 05:18:51 +00:00
Merge pull request #13191 from mkllnk/dfc-reset
Reset stock for absent products in DFC catalog
This commit is contained in:
@@ -14,13 +14,13 @@ module Admin
|
||||
|
||||
def index
|
||||
# Fetch DFC catalog JSON for preview
|
||||
api = DfcRequest.new(spree_current_user)
|
||||
@catalog_url = params.require(:catalog_url).strip
|
||||
@catalog_json = api.call(@catalog_url)
|
||||
catalog = DfcCatalog.from_json(@catalog_json)
|
||||
|
||||
# Render table and let user decide which ones to import.
|
||||
@items = list_products(catalog)
|
||||
@absent_items = importer(catalog).absent_variants
|
||||
rescue URI::InvalidURIError
|
||||
flash[:error] = t ".invalid_url"
|
||||
redirect_to admin_product_import_path
|
||||
@@ -58,6 +58,7 @@ module Admin
|
||||
end
|
||||
|
||||
@count = imported.compact.count
|
||||
@reset_count = importer(catalog).reset_absent_variants.count
|
||||
rescue ActionController::ParameterMissing => e
|
||||
flash[:error] = e.message
|
||||
redirect_to admin_product_import_path
|
||||
@@ -65,6 +66,10 @@ module Admin
|
||||
|
||||
private
|
||||
|
||||
def api
|
||||
@api ||= DfcRequest.new(spree_current_user)
|
||||
end
|
||||
|
||||
def load_enterprise
|
||||
@enterprise = OpenFoodNetwork::Permissions.new(spree_current_user)
|
||||
.managed_product_enterprises.is_primary_producer
|
||||
@@ -80,5 +85,9 @@ module Admin
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
def importer(catalog)
|
||||
DfcCatalogImporter.new(@enterprise.supplied_variants, catalog)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -60,7 +60,11 @@ class OpenOrderCycleJob < ApplicationJob
|
||||
catalog_links.each do |link|
|
||||
catalog_item = catalog.item(link.semantic_id)
|
||||
|
||||
SuppliedProductImporter.update_product(catalog_item, link.subject) if catalog_item
|
||||
if catalog_item
|
||||
SuppliedProductImporter.update_product(catalog_item, link.subject)
|
||||
else
|
||||
DfcCatalogImporter.reset_variant(link.subject)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
51
app/services/dfc_catalog_importer.rb
Normal file
51
app/services/dfc_catalog_importer.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DfcCatalogImporter
|
||||
def self.reset_variant(variant)
|
||||
if variant.on_demand
|
||||
variant.on_demand = false
|
||||
else
|
||||
variant.on_hand = 0
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :catalog, :existing_variants
|
||||
|
||||
def initialize(existing_variants, catalog)
|
||||
@existing_variants = existing_variants
|
||||
@catalog = catalog
|
||||
end
|
||||
|
||||
# Reset stock for any variants that were removed from the catalog.
|
||||
#
|
||||
# When variants are removed from the remote catalog, we can't place
|
||||
# backorders for them anymore. If our copy of the product has limited
|
||||
# stock then we need to set the stock to zero to prevent any more sales.
|
||||
#
|
||||
# But if our product is on-demand/backorderable then our stock level is
|
||||
# a representation of remaining local stock. We then need to limit sales
|
||||
# to this local stock and set on-demand to false.
|
||||
#
|
||||
# We don't delete the variant because it may come back at a later time and
|
||||
# we don't want to lose the connection to previous orders.
|
||||
def reset_absent_variants
|
||||
absent_variants.map do |variant|
|
||||
self.class.reset_variant(variant)
|
||||
end
|
||||
end
|
||||
|
||||
def absent_variants
|
||||
present_ids = catalog.products.map(&:semanticId)
|
||||
catalog_url = FdcUrlBuilder.new(present_ids.first).catalog_url
|
||||
|
||||
existing_variants
|
||||
.includes(:semantic_links).references(:semantic_links)
|
||||
.where.not(semantic_links: { semantic_id: present_ids })
|
||||
.select do |variant|
|
||||
# Variants that were in the same catalog before:
|
||||
variant.semantic_links.map(&:semantic_id).any? do |semantic_id|
|
||||
FdcUrlBuilder.new(semantic_id).catalog_url == catalog_url
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
%tr
|
||||
%td
|
||||
%label
|
||||
❌
|
||||
= absent_variant.product_and_full_name
|
||||
%td
|
||||
= t(".reset")
|
||||
= link_to(absent_variant.product_id, edit_admin_product_path(absent_variant.product_id))
|
||||
@@ -3,5 +3,6 @@
|
||||
|
||||
= render partial: 'spree/admin/shared/product_sub_menu'
|
||||
|
||||
%p= t(".imported_products")
|
||||
= @count
|
||||
%p= t(".imported_products", count: @count)
|
||||
|
||||
%p= t(".reset_products", count: @reset_count) if @reset_count.positive?
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
%p= t('.catalog_url', count: @items.count, catalog_url: @catalog_url)
|
||||
%p= t('.enterprise', enterprise_name: @enterprise.name)
|
||||
%p= t('.absent_products', count: @absent_items.count)
|
||||
%br
|
||||
|
||||
= form_with url: main_app.import_admin_dfc_product_imports_path, html: { "data-controller": "checked" } do |form|
|
||||
@@ -32,6 +33,7 @@
|
||||
= link_to(existing_product.id, edit_admin_product_path(existing_product))
|
||||
- else
|
||||
= t(".new")
|
||||
= render partial: "absent_variant", collection: @absent_items
|
||||
|
||||
%span{ "data-controller": "checked-feedback", "data-checked-feedback-translation-value": "admin.dfc_product_imports.index.selected" }
|
||||
= t(".selected", count: @items.count)
|
||||
|
||||
@@ -725,7 +725,7 @@ el:
|
||||
other: "%{count}επιλέχθηκε"
|
||||
import: Εισαγωγή
|
||||
import:
|
||||
imported_products: "Εισαγόμενα προϊόντα:"
|
||||
imported_products: "Εισαγόμενα προϊόντα: %{count}"
|
||||
enterprise_fees:
|
||||
index:
|
||||
title: "Τέλη επιχείρησης"
|
||||
|
||||
@@ -850,9 +850,19 @@ en:
|
||||
connection_invalid_html: |
|
||||
Connecting with your OIDC account failed.
|
||||
Please refresh your OIDC connection at: %{oidc_settings_link}
|
||||
absent_variant:
|
||||
reset: "Reset stock"
|
||||
index:
|
||||
title: "DFC product catalog"
|
||||
catalog_url: "%{count} products to be imported from: %{catalog_url}"
|
||||
absent_products:
|
||||
zero: ""
|
||||
one: |
|
||||
One product is no longer in the catalog.
|
||||
It will be marked as unavailable by resetting stock to zero.
|
||||
other: |
|
||||
%{count} products are no longer in the catalog.
|
||||
They will be marked as unavailable by resetting stock to zero.
|
||||
enterprise: "Import to enterprise: %{enterprise_name}"
|
||||
select_all: "Select/deselect all"
|
||||
update: Update
|
||||
@@ -865,7 +875,8 @@ en:
|
||||
invalid_url: This catalog URL is not valid.
|
||||
import:
|
||||
title: "DFC product catalog import"
|
||||
imported_products: "Imported products:"
|
||||
imported_products: "Imported products: %{count}"
|
||||
reset_products: "Stock reset for absent products: %{count}"
|
||||
enterprise_fees:
|
||||
index:
|
||||
title: "Enterprise Fees"
|
||||
|
||||
@@ -798,7 +798,7 @@ en_CA:
|
||||
invalid_url: This catalog URL is not valid.
|
||||
import:
|
||||
title: "DFC product catalog import"
|
||||
imported_products: "Imported products:"
|
||||
imported_products: "Imported products: %{count}"
|
||||
enterprise_fees:
|
||||
index:
|
||||
title: "Enterprise Fees"
|
||||
|
||||
@@ -798,7 +798,7 @@ en_FR:
|
||||
invalid_url: This catalog URL is not valid.
|
||||
import:
|
||||
title: "DFC product catalog import"
|
||||
imported_products: "Imported products:"
|
||||
imported_products: "Imported products: %{count}"
|
||||
enterprise_fees:
|
||||
index:
|
||||
title: "Enterprise Fees"
|
||||
|
||||
@@ -771,7 +771,7 @@ en_GB:
|
||||
other: "%{count} selected"
|
||||
import: Import
|
||||
import:
|
||||
imported_products: "Imported products:"
|
||||
imported_products: "Imported products: %{count}"
|
||||
enterprise_fees:
|
||||
index:
|
||||
title: "Enterprise Fees"
|
||||
|
||||
@@ -771,7 +771,7 @@ en_IE:
|
||||
other: "%{count} selected"
|
||||
import: Import
|
||||
import:
|
||||
imported_products: "Imported products:"
|
||||
imported_products: "Imported products: %{count}"
|
||||
enterprise_fees:
|
||||
index:
|
||||
title: "Enterprise Fees"
|
||||
|
||||
@@ -765,7 +765,7 @@ es:
|
||||
new: Nuevo
|
||||
import: Importar
|
||||
import:
|
||||
imported_products: "Productos importados:"
|
||||
imported_products: "Productos importados: %{count}"
|
||||
enterprise_fees:
|
||||
index:
|
||||
title: "Comisiones de la Organización"
|
||||
|
||||
@@ -797,7 +797,7 @@ fr:
|
||||
invalid_url: L'URL de ce catalogue n'est pas valide.
|
||||
import:
|
||||
title: "Import du catalogue produit DFC"
|
||||
imported_products: "Produits importés :"
|
||||
imported_products: "Produits importés : %{count}"
|
||||
enterprise_fees:
|
||||
index:
|
||||
title: "Marges et Commissions"
|
||||
|
||||
@@ -798,7 +798,7 @@ fr_CA:
|
||||
invalid_url: L'URL de ce catalogue n'est pas valide.
|
||||
import:
|
||||
title: "Import du catalogue produit DFC"
|
||||
imported_products: "Produits importés :"
|
||||
imported_products: "Produits importés : %{count}"
|
||||
enterprise_fees:
|
||||
index:
|
||||
title: "Marges et Commissions"
|
||||
|
||||
@@ -760,7 +760,7 @@ hu:
|
||||
other: "%{count}kiválasztva"
|
||||
import: Importálás
|
||||
import:
|
||||
imported_products: "Importált termékek:"
|
||||
imported_products: "Importált termékek: %{count}"
|
||||
enterprise_fees:
|
||||
index:
|
||||
title: "Vállalkozási díjak"
|
||||
|
||||
@@ -771,7 +771,7 @@ nb:
|
||||
other: "%{count} valgt"
|
||||
import: Import
|
||||
import:
|
||||
imported_products: "Importerte produkter:"
|
||||
imported_products: "Importerte produkter: %{count}"
|
||||
enterprise_fees:
|
||||
index:
|
||||
title: "Bedriftsavgifter"
|
||||
|
||||
@@ -711,7 +711,7 @@ ru:
|
||||
other: "выбрано %{count}"
|
||||
import: Импорт
|
||||
import:
|
||||
imported_products: "Внесенные товары:"
|
||||
imported_products: "Внесенные товары: %{count}"
|
||||
enterprise_fees:
|
||||
index:
|
||||
title: "Сборы Предприятия"
|
||||
|
||||
@@ -33,21 +33,34 @@ RSpec.describe OpenOrderCycleJob do
|
||||
|
||||
let(:enterprise) { create(:supplier_enterprise) }
|
||||
let!(:variant) { create(:variant, name: "Sauce", supplier_id: enterprise.id) }
|
||||
let!(:variant_discontinued) {
|
||||
create(:variant, name: "Shiraz 1971", supplier_id: enterprise.id)
|
||||
}
|
||||
let!(:order_cycle) {
|
||||
create(:simple_order_cycle, orders_open_at: now,
|
||||
suppliers: [enterprise], variants: [variant])
|
||||
create(
|
||||
:simple_order_cycle,
|
||||
orders_open_at: now,
|
||||
suppliers: [enterprise],
|
||||
variants: [variant, variant_discontinued]
|
||||
)
|
||||
}
|
||||
|
||||
it "synchronises products from a FDC catalog", vcr: true do
|
||||
user.update!(oidc_account: build(:testdfc_account))
|
||||
# One product is existing in OFN
|
||||
# One current product is existing in OFN
|
||||
product_id =
|
||||
"https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts/44519466467635"
|
||||
variant.semantic_links << SemanticLink.new(semantic_id: product_id)
|
||||
|
||||
# One discontinued product is existing in OFN
|
||||
old_product_id =
|
||||
"https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts/44519466467635-disc"
|
||||
variant_discontinued.semantic_links << SemanticLink.new(semantic_id: old_product_id)
|
||||
|
||||
expect {
|
||||
subject
|
||||
variant.reload
|
||||
variant_discontinued.reload
|
||||
order_cycle.reload
|
||||
}.to change { order_cycle.opened_at }
|
||||
.and change { enterprise.supplied_products.count }.by(0) # It shouldn't add, only update
|
||||
@@ -57,7 +70,8 @@ RSpec.describe OpenOrderCycleJob do
|
||||
.and change { variant.price }.to(1.57)
|
||||
.and change { variant.on_demand }.to(true)
|
||||
.and change { variant.on_hand }.by(0)
|
||||
.and query_database 46
|
||||
.and change { variant_discontinued.on_hand }.to(0)
|
||||
.and query_database 58
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ RSpec.describe "DFC Product Import" do
|
||||
let(:user) { create(:oidc_user, owned_enterprises: [enterprise]) }
|
||||
let(:enterprise) { create(:supplier_enterprise, name: "Saucy preserves") }
|
||||
let(:source_product) { create(:product, name: "Sauce", supplier_id: enterprise.id) }
|
||||
let(:old_product) { create(:product, name: "Best Sauce of 1995", supplier_id: enterprise.id) }
|
||||
|
||||
before do
|
||||
login_as user
|
||||
@@ -52,12 +53,21 @@ RSpec.describe "DFC Product Import" do
|
||||
|
||||
it "imports from a FDC catalog", vcr: true do
|
||||
user.update!(oidc_account: build(:testdfc_account))
|
||||
# One product is existing in OFN
|
||||
|
||||
# One current product is existing in OFN
|
||||
product_id =
|
||||
"https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts/44519466467635"
|
||||
linked_variant = source_product.variants.first
|
||||
linked_variant.semantic_links << SemanticLink.new(semantic_id: product_id)
|
||||
|
||||
# One outdated product still exists in OFN
|
||||
old_product_id =
|
||||
"https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts/445194664-1995"
|
||||
unlinked_variant = old_product.variants.first
|
||||
unlinked_variant.semantic_links << SemanticLink.new(semantic_id: old_product_id)
|
||||
unlinked_variant.on_demand = true
|
||||
unlinked_variant.on_hand = 3
|
||||
|
||||
visit admin_product_import_path
|
||||
|
||||
url = "https://env-0105831.jcloud-ver-jpe.ik-server.com/api/dfc/Enterprises/test-hodmedod/SuppliedProducts"
|
||||
@@ -66,11 +76,13 @@ RSpec.describe "DFC Product Import" do
|
||||
click_button "Preview"
|
||||
|
||||
expect(page).to have_content "4 products to be imported"
|
||||
expect(page).to have_content "One product is no longer"
|
||||
expect(page).to have_content "Saucy preserves"
|
||||
expect(page).not_to have_content "Sauce - 1g" # Does not show other product
|
||||
expect(page).to have_content "Beans - Retail can, 400g (can) Update" # existing product
|
||||
expect(page).to have_content "Beans - Case, 12 x 400g (can) New"
|
||||
expect(page).to have_content "Chia Seed, Organic - Retail pack, 300g"
|
||||
expect(page).to have_content "Best Sauce of 1995 - 1g Reset stock"
|
||||
|
||||
# I can select all
|
||||
uncheck "Chia Seed, Organic - Case, 8 x 300g"
|
||||
@@ -83,14 +95,18 @@ RSpec.describe "DFC Product Import" do
|
||||
expect {
|
||||
click_button "Import"
|
||||
expect(page).to have_content "Imported products: 3"
|
||||
expect(page).to have_content "Stock reset for absent products: 1"
|
||||
linked_variant.reload
|
||||
}.to change { enterprise.supplied_products.count }.by(2) # 1 updated, 2 new
|
||||
unlinked_variant.reload
|
||||
}.to change { enterprise.supplied_products.count }.by(2) # 1 updated, 2 new, 1 reset
|
||||
.and change { linked_variant.display_name }
|
||||
.and change { linked_variant.unit_value }
|
||||
# 18.85 wholesale variant price divided by 12 cans in the slab.
|
||||
.and change { linked_variant.price }.to(1.57)
|
||||
.and change { linked_variant.on_demand }.to(true)
|
||||
.and change { linked_variant.on_hand }.by(0)
|
||||
.and change { unlinked_variant.on_demand }.to(false)
|
||||
.and change { unlinked_variant.on_hand }.by(0)
|
||||
|
||||
product = Spree::Product.last
|
||||
expect(product.variants[0].semantic_links).to be_present
|
||||
|
||||
Reference in New Issue
Block a user