Q:

Order picking in warehouse

In implementing the warehouse management system for an ecommerce store, I'm trying to create a picking list for warehouse workers, who will walk around a warehouse picking products in orders from different shelves. 
One type of product can be on different shelves, and on each shelf there can be many of the same type of product. 
If there are many of the same product in one order, sometimes the picker has to pick from multiple shelves to get all the items in an order.
To further make things trickier, sometimes the product will run out of stock as well.
My data model looks like this (simplified):
CREATE TABLE order_product (
    id SERIAL PRIMARY KEY,
    product_id integer,
    order_id text
);

INSERT INTO "public"."order_product"("id","product_id","order_id")
VALUES
(1,1,'order1'),
(2,1,'order1'),
(3,1,'order1'),
(4,2,'order1'),
(5,2,'order2'),
(6,2,'order2');

CREATE TABLE warehouse_placement (
    id SERIAL PRIMARY KEY,
    product_id integer,
    shelf text,
    quantity integer
);

INSERT INTO "public"."warehouse_placement"("id","product_id","shelf","quantity")
VALUES
(1,1,E'A',2),
(2,2,E'B',2),
(3,1,E'C',2);

Is it possible, in postgres, to generate a picking list of instructions like the following:
order_id product_id shelf quantity_left_on_shelf
order1   1          A     1
order1   1          A     0
order1   2          B     1
order1   1          C     1
order2   2          B     0
order2   2         NONE   null     

I currently do this in the application code, but that feel quite clunky and somehow I feel like there should be a way to do this directly in SQL.
Thanks for any help!

A:

Here we go:
WITH product_on_shelf AS (
    SELECT warehouse_placement.*, 
           generate_series(1, quantity) AS order_on_shelf,
           quantity - generate_series(1, quantity) AS quantity_left_on_shelf
    FROM warehouse_placement
)
, product_on_shelf_with_product_order AS (
    SELECT *,
           ROW_NUMBER() OVER (
               PARTITION BY product_id 
               ORDER BY quantity, shelf, order_on_shelf
           ) AS order_among_product
    FROM product_on_shelf
)
, order_product_with_order_among_product AS (
    SELECT *,
           ROW_NUMBER() OVER (
               PARTITION BY product_id 
               ORDER BY id
           ) AS order_among_product
    FROM order_product
)
SELECT order_product_with_order_among_product.id,
       order_product_with_order_among_product.order_id,
       order_product_with_order_among_product.product_id,
       product_on_shelf_with_product_order.shelf,
       product_on_shelf_with_product_order.quantity_left_on_shelf
FROM order_product_with_order_among_product
LEFT JOIN product_on_shelf_with_product_order
    ON order_product_with_order_among_product.product_id = product_on_shelf_with_product_order.product_id
    AND order_product_with_order_among_product.order_among_product = product_on_shelf_with_product_order.order_among_product
ORDER BY order_product_with_order_among_product.id
;

Here's the idea:

We create a temporary table product_on_shelf, which is the same as warehouse_placement, except the rows are duplicated n times, n being the quantity of the product on the shelf.
We assign a number order_among_product to each row in product_on_shelf, so that each object on shelf knows its order among the same products.
We assign a symmetric number order_among_product to each row in order_product.
For each row in order_product, we try to find the product on shelf with the same order_among_product. If we can't find any, it means we've exhausted the products on any shelf.

Side note #1: Picking products off shelves is a concurrent action. You should make sure, either on the application side or on the DB side via smart locks, that any product on shelf can be attributed to one single order. Treating each row of product_order on the application side might be the best option to deal with concurrence.
Side note #2: I've written this query using CTEs for clarity. To boost performance, consider using subqueries instead. Make sure to run EXPLAIN ANALYZE

