mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-02 21:57:17 +00:00
Amend backorder completely
Update every single order line to reflect local orders and stock levels. New cases supported: * Add lines for orders created by an admin. * Create backorder line after increase of local line item quantity. * Adjust local stock after variant has been removed from order cycle.
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# When orders are cancelled, we need to amend
|
||||
require 'open_food_network/order_cycle_permissions'
|
||||
|
||||
# When orders are created, adjusted or cancelled, we need to amend
|
||||
# an existing backorder as well.
|
||||
# We're not dealing with line item changes just yet.
|
||||
class AmendBackorderJob < ApplicationJob
|
||||
sidekiq_options retry: 0
|
||||
|
||||
@@ -15,81 +16,119 @@ class AmendBackorderJob < ApplicationJob
|
||||
# The following is a mix of the BackorderJob and the CompleteBackorderJob.
|
||||
# TODO: Move the common code into a re-usable service class.
|
||||
def amend_backorder(order)
|
||||
order_cycle = order.order_cycle
|
||||
distributor = order.distributor
|
||||
user = distributor.owner
|
||||
items = backorderable_items(order)
|
||||
variants = distributed_linked_variants(order)
|
||||
return unless variants.any?
|
||||
|
||||
return if items.empty?
|
||||
user = order.distributor.owner
|
||||
order_cycle = order.order_cycle
|
||||
|
||||
# We are assuming that all variants are linked to the same wholesale
|
||||
# shop and its catalog:
|
||||
reference_link = items[0].variant.semantic_links[0].semantic_id
|
||||
reference_link = variants[0].semantic_links[0].semantic_id
|
||||
urls = FdcUrlBuilder.new(reference_link)
|
||||
orderer = FdcBackorderer.new(user, urls)
|
||||
broker = FdcOfferBroker.new(user, urls)
|
||||
|
||||
backorder = orderer.find_open_order(order)
|
||||
|
||||
variants = order_cycle.variants_distributed_by(distributor)
|
||||
adjust_quantities(order_cycle, user, backorder, urls, variants)
|
||||
updated_lines = update_order_lines(backorder, order_cycle, variants, broker, orderer)
|
||||
unprocessed_lines = backorder.lines.to_set - updated_lines
|
||||
cancel_stale_lines(unprocessed_lines, order, broker)
|
||||
|
||||
# Clean up empty lines:
|
||||
backorder.lines.reject! { |line| line.quantity.zero? }
|
||||
|
||||
FdcBackorderer.new(user, urls).send_order(backorder)
|
||||
end
|
||||
|
||||
# Check if we have enough stock to reduce the backorder.
|
||||
#
|
||||
# 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_cycle, user, order, urls, variants)
|
||||
broker = FdcOfferBroker.new(user, urls)
|
||||
def update_order_lines(backorder, order_cycle, variants, broker, orderer)
|
||||
variants.map do |variant|
|
||||
link = variant.semantic_links[0].semantic_id
|
||||
solution = broker.best_offer(link)
|
||||
line = orderer.find_or_build_order_line(backorder, solution.offer)
|
||||
if variant.on_demand
|
||||
adjust_stock(variant, solution, line)
|
||||
else
|
||||
aggregate_final_quantities(order_cycle, line, variant, solution)
|
||||
end
|
||||
|
||||
order.lines.each do |line|
|
||||
line.quantity = line.quantity.to_i
|
||||
line
|
||||
end
|
||||
end
|
||||
|
||||
def cancel_stale_lines(unprocessed_lines, order, broker)
|
||||
managed_variants = managed_linked_variants(order)
|
||||
unprocessed_lines.each do |line|
|
||||
wholesale_quantity = line.quantity.to_i
|
||||
wholesale_product_id = line.offer.offeredItem.semanticId
|
||||
transformation = broker.wholesale_to_retail(wholesale_product_id)
|
||||
linked_variant = variants.linked_to(transformation.retail_product_id)
|
||||
linked_variant = managed_variants.linked_to(transformation.retail_product_id)
|
||||
|
||||
# Assumption: If a transformation is present then we only sell the retail
|
||||
# variant. If that can't be found, it was deleted and we'll ignore that
|
||||
# for now.
|
||||
next if linked_variant.nil?
|
||||
if linked_variant.nil?
|
||||
transformation.factor = 1
|
||||
linked_variant = managed_variants.linked_to(wholesale_product_id)
|
||||
end
|
||||
|
||||
# Find all line items for this order cycle
|
||||
# Update quantity accordingly
|
||||
if linked_variant.on_demand
|
||||
release_superfluous_stock(line, linked_variant, transformation)
|
||||
else
|
||||
aggregate_final_quantities(order_cycle, line, linked_variant, transformation)
|
||||
# Adjust stock level back, we're not going to order this one.
|
||||
if linked_variant&.on_demand
|
||||
retail_quantity = wholesale_quantity * transformation.factor
|
||||
linked_variant.on_hand -= retail_quantity
|
||||
end
|
||||
|
||||
# We don't have any active orders for this
|
||||
line.quantity = 0
|
||||
end
|
||||
end
|
||||
|
||||
def adjust_stock(variant, solution, line)
|
||||
if variant.on_hand.negative?
|
||||
needed_quantity = -1 * variant.on_hand # We need to replenish it.
|
||||
|
||||
# The number of wholesale packs we need to order to fulfill the
|
||||
# needed quantity.
|
||||
# For example, we order 2 packs of 12 cans if we need 15 cans.
|
||||
wholesale_quantity = (needed_quantity.to_f / solution.factor).ceil
|
||||
|
||||
# The number of individual retail items we get with the wholesale order.
|
||||
# For example, if we order 2 packs of 12 cans, we will get 24 cans
|
||||
# and we'll account for that in our stock levels.
|
||||
retail_quantity = wholesale_quantity * solution.factor
|
||||
|
||||
line.quantity = line.quantity.to_i + wholesale_quantity
|
||||
variant.on_hand += retail_quantity
|
||||
else
|
||||
# Note that a division of integers dismisses the remainder, like `floor`:
|
||||
wholesale_items_contained_in_stock = variant.on_hand / solution.factor
|
||||
|
||||
# But maybe we didn't actually order that much:
|
||||
deductable_quantity = [line.quantity, wholesale_items_contained_in_stock].min
|
||||
|
||||
if deductable_quantity.positive?
|
||||
line.quantity -= deductable_quantity
|
||||
|
||||
retail_stock_change = deductable_quantity * solution.factor
|
||||
variant.on_hand -= retail_stock_change
|
||||
end
|
||||
end
|
||||
|
||||
# Clean up empty lines:
|
||||
order.lines.reject! { |line| line.quantity.zero? }
|
||||
end
|
||||
|
||||
# We look at all linked variants.
|
||||
def backorderable_items(order)
|
||||
order.line_items.select do |item|
|
||||
# TODO: scope variants to hub.
|
||||
# We are only supporting producer stock at the moment.
|
||||
item.variant.semantic_links.present?
|
||||
end
|
||||
def managed_linked_variants(order)
|
||||
user = order.distributor.owner
|
||||
order_cycle = order.order_cycle
|
||||
|
||||
# These permissions may be too complex. Here may be scope to optimise.
|
||||
permissions = OpenFoodNetwork::OrderCyclePermissions.new(user, order_cycle)
|
||||
permissions.visible_variants_for_outgoing_exchanges_to(order.distributor)
|
||||
.where.associated(:semantic_links)
|
||||
end
|
||||
|
||||
def release_superfluous_stock(line, linked_variant, transformation)
|
||||
# Note that a division of integers dismisses the remainder, like `floor`:
|
||||
wholesale_items_contained_in_stock = linked_variant.on_hand / transformation.factor
|
||||
|
||||
# But maybe we didn't actually order that much:
|
||||
deductable_quantity = [line.quantity, wholesale_items_contained_in_stock].min
|
||||
line.quantity -= deductable_quantity
|
||||
|
||||
retail_stock_changes = deductable_quantity * transformation.factor
|
||||
linked_variant.on_hand -= retail_stock_changes
|
||||
def distributed_linked_variants(order)
|
||||
order.order_cycle.variants_distributed_by(order.distributor)
|
||||
.where.associated(:semantic_links)
|
||||
end
|
||||
|
||||
def aggregate_final_quantities(order_cycle, line, variant, transformation)
|
||||
# We may want to query all these quantities in one go instead of this n+1.
|
||||
orders = order_cycle.orders.invoiceable
|
||||
quantity = Spree::LineItem.where(order: orders, variant:).sum(:quantity)
|
||||
wholesale_quantity = (quantity.to_f / transformation.factor).ceil
|
||||
|
||||
@@ -86,13 +86,33 @@ RSpec.describe AmendBackorderJob do
|
||||
expect(backorder.lines[0].quantity).to eq 1 # beans
|
||||
expect(backorder.lines[1].quantity).to eq 5 # chia
|
||||
|
||||
# We cancel the only order and that should reduce the order lines to 0.
|
||||
expect { order.cancel! }
|
||||
.to change { beans.reload.on_hand }.from(9).to(15)
|
||||
.and change { chia_seed.reload.on_hand }.from(7).to(12)
|
||||
# We increase quantities which should be reflected in the backorder:
|
||||
beans.on_hand = -1
|
||||
chia_item.quantity += 3
|
||||
chia_item.save!
|
||||
|
||||
expect { subject.amend_backorder(order) }
|
||||
.to change { backorder.lines.count }.from(2).to(0)
|
||||
.to change { beans.on_hand }.from(-1).to(11)
|
||||
.and change { backorder.lines[0].quantity }.from(1).to(2)
|
||||
.and change { backorder.lines[1].quantity }.from(5).to(8)
|
||||
|
||||
# We cancel the only order.
|
||||
expect { order.cancel! }
|
||||
.to change { beans.reload.on_hand }.from(11).to(17)
|
||||
.and change { chia_seed.reload.on_hand }.from(4).to(12)
|
||||
|
||||
# But we decreased the stock of beans outside of orders above.
|
||||
# So only the chia seeds are cancelled. The beans still need replenishing.
|
||||
expect { subject.amend_backorder(order) }
|
||||
.to change { backorder.lines.count }.from(2).to(1)
|
||||
.and change { beans.reload.on_hand }.by(-12)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#distributed_linked_variants" do
|
||||
it "selects available variants with semantic links" do
|
||||
variants = subject.distributed_linked_variants(order)
|
||||
expect(variants).to match_array [beans, chia_seed]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user