Given:
2 tables: sales and purchases
- Sales: product_id, count, date
- Purchases: product_id, purchase number, count of items in the purchase, cost per unit
Task:
To calculate the cost of goods sold using the FIFO (first in – first out, first in – first out) model. Deliveries are ranked from older to newer by increasing delivery number. Sales write off the cost of goods first from the first shipment, when it runs out from the second shipment, and so on.
What I’ve tried:
Sorted the sales by date and id and made windows cumulative sum of the number of sales. But here is how to link to a specific purchase and move to the next one, in case the quantity from the purchase is over I can not understand.
SELECT
"Product ID" AS product_id,
"Date" AS date,
"Sales QTY" AS sales_qty,
SUM("Sales QTY") OVER (PARTITION BY "Product ID"
ORDER BY "Date" ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cummulative_sale_sum
FROM "SALES"
ORDER BY product_id, date
I used purchase cumulatives and subtraction, but as a result I get wrong amounts since the 2nd month.
WITH sales_grouped AS (
SELECT
"Product ID",
SUM("Sales QTY") AS total_sales_qty,
to_char("Date", 'YYYY-MM') AS year_month
FROM "SALES"
GROUP BY "Product ID", to_char("Date", 'YYYY-MM')
),
supply_cumulative AS (
SELECT
"Product ID",
"Supply QTY",
"#Supply",
"Costs Per PCS",
SUM("Supply QTY") OVER (PARTITION BY "Product ID" ORDER BY "#Supply") AS cumulative_qty
FROM "SUPPLY"
),
matched_sales AS (
SELECT
s."Product ID",
s.year_month,
s.total_sales_qty,
c."Supply QTY",
c."Costs Per PCS",
c.cumulative_qty - c."Supply QTY" AS prev_cumulative_qty
FROM sales_grouped s
JOIN supply_cumulative c
ON s."Product ID" = c."Product ID"
),
fifo_calculation AS (
SELECT
"Product ID",
year_month,
"Costs Per PCS",
CASE
WHEN total_sales_qty > prev_cumulative_qty THEN
LEAST(total_sales_qty - prev_cumulative_qty, "Supply QTY")
ELSE 0
END AS used_qty
FROM matched_sales
)
SELECT
"Product ID",
year_month,
SUM(used_qty * "Costs Per PCS") AS total_cost
FROM fifo_calculation
GROUP BY "Product ID", year_month
ORDER BY "Product ID", year_month;
2
Answers
To actual match sales with purchases in the order of purchases. Your best bet is
You can use this code snippet:
Your question takes a little bit of methodology. There are 3 parts in it
For reasons that will become clear later, I will address the points in order 1, 3 and finally 2.
Cumulative quantities
This is the easy part: it consists in calculating cumulative sums for each product; for example:
Calculating the quantities
For the sake of the explanation, let’s assume we are able to correctly join the
purchases
(in green below) andsales
(in red) together. The quantity to output depends on the purchase/sold counts as well as the stock and total sold on the joined record.A visual representation is going to be helpful. Each record falls into one of 4 configurations:
This must be read as:
This includes the situation where segments have the same end(s).
Respective to the above description, the quantity to be returned can be calculated as one of:
count
total_sold - previous(stock)
stock - previous(total_sold)
count
The right quantity can be chosen with a
CASE WHEN ... ELSE ...
but it is very verbose. Alternatively, a simpleLEAST
should have the same effect.Joining purchases and sales
The
JOIN
follows the very same rules as above, but this time consists in looking at all the records at once, like in the below fashion:This must be read as:
I have not covered the 4th case from above in that visual representation but anyhow, it should make it slightly easier to write the correct join condition:
SQL
For the resulting SQL, it must be noted there were discrepancies in the column names from your description and your code, with spaces in both sides. I therefore went with my own names, without spaces.
As to decrease the number of CTEs, the
previous(stock)
andprevious(total_sold)
from above must respectively be replaced by(stock - s.count)
(s
being the alias for thestock
CTE) and(total_sold - ts.count)
ts
being the alias for thetotal_sale
CTE):Note that you may want/need to change the type of join for a
INNER JOIN
if you want to only keep sales (theLEFT JOIN
keeps purchase #4 from my above visual representation).You can also use a
RIGHT JOIN
to see if, for any reason, there are more sales than purchases (it is not supposed to be possible).