mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-04 22:16:08 +00:00
Adjust quantities of backorder before completion
This commit is contained in:
@@ -87,7 +87,9 @@ class BackorderJob < ApplicationJob
|
||||
|
||||
wait_until = order.order_cycle.orders_close_at + SALE_SESSION_DELAY
|
||||
CompleteBackorderJob.set(wait_until:)
|
||||
.perform_later(user, placed_order.semanticId)
|
||||
.perform_later(
|
||||
user, order.distributor, order.order_cycle, placed_order.semanticId
|
||||
)
|
||||
end
|
||||
|
||||
def perform(*args)
|
||||
|
||||
@@ -3,12 +3,25 @@
|
||||
# After an order cycle closed, we need to finalise open draft orders placed
|
||||
# to replenish stock.
|
||||
class CompleteBackorderJob < ApplicationJob
|
||||
def perform(user, order_id)
|
||||
# TODO: review our stock levels and adjust quantities if we got surplus.
|
||||
# This can happen when orders are cancelled and products restocked.
|
||||
# Required parameters:
|
||||
#
|
||||
# * user: to authenticate DFC requests
|
||||
# * distributor: to reconile with its catalog
|
||||
# * order_cycle: to scope the catalog when looking up variants
|
||||
# Multiple variants can be linked to the same remote product.
|
||||
# To reduce ambiguity, we'll reconcile only with products
|
||||
# from the given distributor in a given order cycle for which
|
||||
# the remote backorder was placed.
|
||||
# * order_id: the remote semantic id of a draft order
|
||||
# Having the id makes sure that we don't accidentally finalise
|
||||
# someone else's order.
|
||||
def perform(user, distributor, order_cycle, order_id)
|
||||
service = FdcBackorderer.new(user)
|
||||
order = service.find_order(order_id)
|
||||
adjust_quantities(order)
|
||||
|
||||
variants = order_cycle.variants_distributed_by(distributor)
|
||||
adjust_quantities(user, order, variants)
|
||||
|
||||
service.complete_order(order)
|
||||
end
|
||||
|
||||
@@ -17,7 +30,20 @@ class CompleteBackorderJob < ApplicationJob
|
||||
# Our local stock can increase when users cancel their orders.
|
||||
# But stock levels could also have been adjusted manually. So we review all
|
||||
# quantities before finalising the order.
|
||||
def adjust_quantities(order)
|
||||
# TODO
|
||||
def adjust_quantities(user, order, variants)
|
||||
broker = FdcOfferBroker.new(BackorderJob.load_catalog(user))
|
||||
|
||||
order.lines.each do |line|
|
||||
wholesale_product_id = line.offer.offeredItem.semanticId
|
||||
transformation = broker.wholesale_to_retail(wholesale_product_id)
|
||||
linked_variant = variants.linked_to(transformation.retail_product_id)
|
||||
|
||||
# Note that a division of integers dismisses the remainder, like `floor`:
|
||||
wholesale_items_contained_in_stock = linked_variant.on_hand / transformation.factor
|
||||
line.quantity = line.quantity.to_i - wholesale_items_contained_in_stock
|
||||
|
||||
retail_stock_changes = wholesale_items_contained_in_stock * transformation.factor
|
||||
linked_variant.on_hand -= retail_stock_changes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
# Finds wholesale offers for retail products.
|
||||
class FdcOfferBroker
|
||||
# TODO: Find a better way to provide this data.
|
||||
Solution = Struct.new(:product, :factor, :offer)
|
||||
RetailSolution = Struct.new(:retail_product_id, :factor)
|
||||
|
||||
def initialize(catalog)
|
||||
@catalog = catalog
|
||||
@@ -14,13 +16,25 @@ class FdcOfferBroker
|
||||
|
||||
contained_quantity = consumption_flow.quantity.value.to_i
|
||||
wholesale_product_id = production_flow.product
|
||||
wholesale_product = catalog_item(wholesale_product_id )
|
||||
wholesale_product = catalog_item(wholesale_product_id)
|
||||
|
||||
offer = offer_of(wholesale_product)
|
||||
|
||||
Solution.new(wholesale_product, contained_quantity, offer)
|
||||
end
|
||||
|
||||
def wholesale_to_retail(wholesale_product_id)
|
||||
production_flow = flow_producing(wholesale_product_id)
|
||||
consumption_flow = catalog_item(
|
||||
production_flow.semanticId.sub("AsPlannedProductionFlow", "AsPlannedConsumptionFlow")
|
||||
)
|
||||
retail_product_id = consumption_flow.product
|
||||
|
||||
contained_quantity = consumption_flow.quantity.value.to_i
|
||||
|
||||
RetailSolution.new(retail_product_id, contained_quantity)
|
||||
end
|
||||
|
||||
def offer_of(product)
|
||||
product&.catalogItems&.first&.offers&.first&.tap do |offer|
|
||||
# Unfortunately, the imported catalog doesn't provide the reverse link:
|
||||
@@ -32,4 +46,15 @@ class FdcOfferBroker
|
||||
@catalog_by_id ||= @catalog.index_by(&:semanticId)
|
||||
@catalog_by_id[id]
|
||||
end
|
||||
|
||||
def flow_producing(wholesale_product_id)
|
||||
@production_flows_by_product_id ||= production_flows.index_by(&:product)
|
||||
@production_flows_by_product_id[wholesale_product_id]
|
||||
end
|
||||
|
||||
def production_flows
|
||||
@production_flows ||= @catalog.select do |i|
|
||||
i.semanticType == "dfc-b:AsPlannedProductionFlow"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -24,7 +24,11 @@ RSpec.describe BackorderJob do
|
||||
end
|
||||
|
||||
it "places an order", vcr: true do
|
||||
order.order_cycle = build(:order_cycle)
|
||||
order.order_cycle = create(
|
||||
:simple_order_cycle,
|
||||
distributors: [order.distributor],
|
||||
variants: [variant],
|
||||
)
|
||||
variant.on_demand = true
|
||||
variant.on_hand = -3
|
||||
variant.semantic_links << SemanticLink.new(
|
||||
|
||||
@@ -5,26 +5,60 @@ require 'spec_helper'
|
||||
RSpec.describe CompleteBackorderJob do
|
||||
let(:user) { build(:testdfc_user) }
|
||||
let(:catalog) { BackorderJob.load_catalog(user) }
|
||||
let(:product) {
|
||||
let(:retail_product) {
|
||||
catalog.find { |item| item.semanticType == "dfc-b:SuppliedProduct" }
|
||||
}
|
||||
let(:wholesale_product) {
|
||||
flow = catalog.find { |item| item.semanticType == "dfc-b:AsPlannedProductionFlow" }
|
||||
catalog.find { |item| item.semanticId == flow.product }
|
||||
}
|
||||
let(:orderer) { FdcBackorderer.new(user) }
|
||||
let(:order) {
|
||||
ofn_order = build(:order, distributor_id: 1)
|
||||
ofn_order.order_cycle = build(:order_cycle)
|
||||
backorder = orderer.find_or_build_order(ofn_order)
|
||||
offer = FdcOfferBroker.new(nil).offer_of(product)
|
||||
broker = FdcOfferBroker.new(catalog)
|
||||
offer = broker.best_offer(retail_product.semanticId).offer
|
||||
line = orderer.find_or_build_order_line(backorder, offer)
|
||||
line.quantity = 3
|
||||
|
||||
orderer.send_order(backorder)
|
||||
}
|
||||
let(:ofn_order) { create(:completed_order_with_totals) }
|
||||
let(:distributor) { ofn_order.distributor }
|
||||
let(:order_cycle) { ofn_order.order_cycle }
|
||||
let(:variant) { ofn_order.variants[0] }
|
||||
|
||||
describe "#perform" do
|
||||
before do
|
||||
variant.semantic_links << SemanticLink.new(
|
||||
semantic_id: retail_product.semanticId
|
||||
)
|
||||
|
||||
# We are assuming 12 cans in a slab.
|
||||
# We got more stock than we need.
|
||||
variant.on_hand = 13
|
||||
|
||||
ofn_order.order_cycle = create(
|
||||
:simple_order_cycle,
|
||||
distributors: [distributor],
|
||||
variants: [variant],
|
||||
)
|
||||
end
|
||||
|
||||
it "completes an order", vcr: true do
|
||||
subject.perform(user, order.semanticId)
|
||||
updated_order = orderer.find_order(order.semanticId)
|
||||
expect(updated_order.orderStatus[:path]).to eq "Complete"
|
||||
current_order = order
|
||||
|
||||
expect {
|
||||
subject.perform(user, distributor, order_cycle, order.semanticId)
|
||||
current_order = orderer.find_order(order.semanticId)
|
||||
}.to change {
|
||||
current_order.orderStatus[:path]
|
||||
}.from("Held").to("Complete")
|
||||
.and change {
|
||||
current_order.lines[0].quantity.to_i
|
||||
}.from(3).to(2)
|
||||
.and change {
|
||||
variant.on_hand
|
||||
}.from(13).to(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user