David Minor
David Minor

Reputation: 632

Calculate Profit Based on First-In, First-Out Pricing

Say I have purchase and sales data for some SKUs:

po_id | sku | purchase_date    | price | qty
----------------------------------------------
    1 | 123 | 2013-01-01 12:25 | 20.15 |   5
    2 | 123 | 2013-05-01 15:45 | 17.50 |   3
    3 | 123 | 2013-05-02 12:00 | 15.00 |   1
    4 | 456 | 2013-06-10 16:00 | 60.00 |   7

sale_id | sku | sale_date        | price | qty
------------------------------------------------
      1 | 123 | 2013-01-15 11:00 | 30.00 |   1
      2 | 123 | 2013-01-20 14:00 | 28.00 |   3
      3 | 123 | 2013-05-10 15:00 | 25.00 |   2
      4 | 456 | 2013-06-11 12:00 | 80.00 |   1

How can I find the sales margin via SQL, assuming they are sold in the order they were purchased? E.g, the margin for sku 123 is

30*1 + 28*3 + 25*2 - 20.15*5 - 17.50*1

with 2 purchased at 17.50 and 1 purchased at 15.00 left unsold.

Upvotes: 14

Views: 5156

Answers (4)

Frederic
Frederic

Reputation: 1028

setting ambient

    declare @purchased table (id int,sku int,dt date,price money,qty int)
    declare @sold table (id int,sku int,dt date,price money,qty int)

    insert into @purchased
    values( 1 , 123 , '2013-01-01 12:25' , 20.15 ,   5)
        ,(2 , 123 , '2013-05-01 15:45' , 17.50 ,   3)
        ,(3 , 123 , '2013-05-02 12:00' , 15.00 ,   1)
        ,(4 , 456 , '2013-06-10 16:00' , 60.00 ,   7)

    insert into @sold
    values(1 , 123 , '2013-01-15 11:00' , 30.00 ,   1)
          ,(2 , 123 , '2013-01-20 14:00' , 28.00 ,   3)
          ,(3 , 123 , '2013-05-10 15:00' , 25.00 ,   2)
          ,(4 , 456 , '2013-06-11 12:00' , 80.00 ,   1)

a sqlserver solution should be...

    with cte_sold as (select sku,sum(qty) as qty, SUM(qty*price) as total_value
                      from @sold
                      group by sku
                      )
    ,cte_purchased as (select id,sku,price,qty 
                       from @purchased
                       union all select id,sku,price,qty-1 as qty 
                       from cte_purchased
                       where qty>1
                    )
    ,cte_purchased_ordened as(select ROW_NUMBER() over (partition by sku order by id,qty) as buy_order
                                    ,sku
                                    ,price
                                    ,1 as qty
                              from cte_purchased 
    )

    select P.sku
          ,S.total_value - SUM(case when P.buy_order <= S.qty then P.price else 0 end) as margin
    from cte_purchased_ordened P
    left outer join cte_sold S
    on S.sku = P.sku
    group by P.sku,S.total_value,S.qty

resultset achieved

    sku margin
    123 45,75
    456 20,00

same result for sku 123 example in the problem description...

30*1 + 28*3 + 25*2 - 20.15*5 - 17.50*1 = 45.75

Upvotes: 1

Gordon Linoff
Gordon Linoff

Reputation: 1270773

Good question. The approach that I'm taking is to calculate the total sales. Then calculate cumulative purchases, and combine them with special logic to get the right arithmetic for the combination:

select s.sku,
       (MarginPos - SUM(case when s.totalqty < p.cumeqty - p.qty then p.price * p.qty
                             when s.totalqty between p.cumeqty - p.qty and p.qty
                             then s.price * (s.totalqty - (p.cumeqty - p.qty))
                             else 0
                        end)
       ) as Margin
from (select s.sku, SUM(price*qty) as MarginPos, SUM(qty) as totalqty
      from sales s
     ) s left outer join
     (select p.*,
             (select SUM(p.qty) from purchase p2 where p2.sku = p.sku and p2.sale_id <= p.sale_id
             ) as cumeqty
      from purchase s
     )
     on s.sku = p.sku
group by s.sku, MarginPos

Note: I haven't tested this query so it might have syntax errors.

Upvotes: 8

user645280
user645280

Reputation:

This is really horrible since it changes a MySQL variable in the queries, but it kind of works (and takes 3 statements):

select
  @income := sum(price*qty) as income,
  @num_bought := cast(sum(qty) as unsigned) as units
from sale
where sku = 123
;

select
  @expense := sum(expense) as expense,
  sum(units) as units
from (select
  price * least(@num_bought, qty) as expense,
  least(@num_bought, qty) as units,
  @num_bought := @num_bought - least(@num_bought, qty)
from purchase
where sku = 123 and @num_bought > 0
order by po_id
) as a
;

select round(@income - @expense, 2) as profit_margin;

Upvotes: 0

Art
Art

Reputation: 5792

This is Oracle query but should work in any SQL. It is simplified and does not include all necessary calculations. You can add them yourself. You will see slightly diff totals as 17.50*3 not 17.50*1:

SELECT po_sku AS sku, po_total, sale_total, (po_total-sale_total) Margin
  FROM
 (
  SELECT SUM(price*qty) po_total, sku po_sku
    FROM stack_test
  GROUP BY sku
 ) a,
 (
  SELECT SUM(price*qty) sale_total, sku sale_sku
    FROM stack_test_sale
   GROUP BY sku
 ) b
 WHERE po_sku = sale_sku
 /

SKU     PO_TOTAL    SALE_TOTAL  MARGIN
---------------------------------------------------
123     168.25      164         4.25
456     420         80          340

You can also add partition by SKU if required:

SUM(price*qty) OVER (PARTITION BY sku ORDER BY sku)

Upvotes: -1

Related Questions