-1

I have a purchasing table that stores all orders that a store receives. In some cases, I have to re-order the an item at a different cost. When I make a sale transaction, I need to be able to determine the total cost of the item sold using FIFO (First In, First Our) system determined by the purchased date.

Here is how my table look like

CREATE TABLE `purchases` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `item_id` int(11) unsigned NOT NULL,
  `purchased_on` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `cost` decimal(10,2) unsigned NOT NULL,
  `availableQty` int(11) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `item_id` (`item_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

INSERT INTO `purchases` (`id`, `item_id`, `purchased_on`, `cost`, `availableQty`) VALUES
(1, 1, '2015-01-13 23:50:09', '1.00', 10),
(2, 1, '2015-01-13 23:50:10', '0.90', 20),
(3, 1, '2015-01-13 23:50:11', '0.80', 50),
(4, 2, '2015-01-13 23:50:11', '10.80', 18),
(5, 2, '2015-01-13 23:50:11', '20.25', 235);

Using the data above, if some one want to purchase 40 units of item_id = 1, then the cost is calculated like so (10 X 1.00) + (20 X 0.90) + (10 X 0.80) = 36 . The total cost for this transaction for item_id = 1 is $36.

But now, I need to update the availableQty column with the new available values.

Based on the values in the availableQty column, row 1 and 2 should have a value of 0 in the availableQty column and 40 left for row 3 of the same column.

To calculate the cost of the transaction, I was able to write this query that will do the cost calculation.

SET @orgMyQty = 40;
SET @myQty = @orgMyQty;
SELECT SUM(cost) AS total_cost, SUM(availableQty) AS totalAvailable, SUM(availableQty) - @orgMyQty AS RemainingUnites
FROM (
    SELECT 
    CASE WHEN availableQty >= @myQty THEN cost * @myQty
         ELSE cost * availableQty END AS cost,
    CASE WHEN @myQty > availableQty THEN @myQty := @myQty - availableQty END AS left_over,
    availableQty
    FROM purchases
    WHERE item_id = 1 AND @myQty > 0
    ORDER BY purchased_on ASC, item_id ASC
) AS t

But How Can I update each row with the new totalAvailable Value?

so the final records will need to look like this

(1, 1, '2015-01-13 23:50:09', '1.00', 0),
(2, 1, '2015-01-13 23:50:10', '0.90', 0),
(3, 1, '2015-01-13 23:50:11', '0.80', 40),
(4, 2, '2015-01-13 23:50:11', '10.80', 18),
(5, 2, '2015-01-13 23:50:11', '20.25', 235);

So my questions is this

  1. How to execute the UPDATE correctly?
  2. How to use the query query to lock the rows for update so no other transaction can change the value until this transaction is committed. Note, both queries select and update are in the same transaction.
  3. Is there a better approach to calculate the cost?

I tried the following query to update the values

-- This is giving me a syntax error on this line @myQty := CASE WHEN @myQty > availableQty THEN @myQty - availableQty ELSE @myQty END

SET @myQty = 40;
UPDATE data_import.purchases
SET qty = CASE WHEN @myQty = 0 THEN 0
               WHEN @myQty > 0 AND availableQty >= @myQty THEN cost * @myQty
               ELSE cost * availableQty END,
@myQty := CASE WHEN @myQty > availableQty THEN @myQty - availableQty  ELSE @myQty END
WHERE item_id = 1
ORDER BY purchased_on ASC, item_id ASC;

I also tried this query

-- there is not syntax error here but only the last column was updated incorrectly.
    UPDATE data_import.purchases
    SET qty = CASE WHEN @myQty = 0 THEN 0
                   WHEN @myQty > 0 AND availableQty >= @myQty THEN cost * @myQty
                   ELSE cost * availableQty END
    WHERE item_id = 1 AND @myQty := CASE WHEN @myQty > availableQty THEN @myQty - availableQty  ELSE @myQty END
    ORDER BY purchased_on ASC;
Jaylen
  • 39,043
  • 40
  • 128
  • 221
  • why you should have 0,0,40 ? I assume it should be 0,10,30 – Iłya Bursov Jan 14 '15 at 01:18
  • how do you know that 10,20,10 was purchased? you don't have these numbers in your data – Iłya Bursov Jan 14 '15 at 01:20
  • @Lashane row 1 has 10 then row 2 have 20 ( 10+ 20) = 30, but we need to purchase 40. so you will need to take 10 form 50 to end up with 40. EDITED: the the customer is buying 40 items of the same unit. So I need to look at what is available TOP to BOTTOM and calculate that way – Jaylen Jan 14 '15 at 01:20
  • I don't understand why some people just like to mark questions down without leaving a valid explanation!!!! I do not see any reason that cause someone to mark this question down! – Jaylen Jan 14 '15 at 01:23

1 Answers1

1

here is a little bit different solution to calculate cost:

select sum(cost *
least(
  availableQty,
    greatest(
      0,
      40-ifnull(
        (select sum(availableQty) from purchases as b where b.id<a.id),
        0
      )
    )
  ))
as totalCost
from purchases as a
where item_id = 1

some notes how it works:

Return how many we still should buy for current row:

greatest(
    0,
    40 - ifnull((select sum(availableQty) from purchases as b where b.id<a.id),0)
)

For each row we calculate how many we bought from previous rows, ie we summarize available quantity for all previous rows, subtract this number from 40 (amount we want to buy) and we have rest.

This is basically the same loop you already have, but you're decreasing "rest" every iteration, I recalculate whole previous sum and find difference between it and total demand, so I get rest for each row without loop.

But we can have negative number, this is why we use greatest. So for positive values (ie previous rows didn't fulfill demand) we get how many we still need to select from other rows, for all others - 0, ie all demand is satisfied.

How many we can "buy" from current row:

least(
      availableQty,
      demandRest /* construction above */
      )

We just select either whole available quantity or unfulfilled demand, depending which is smaller

So, basically for each row we either get whole available quantity, or demand rest or 0 if demand was fulfilled by previous rows

here is solution for update:

update purchases as u
inner join
(
select id,
least(
  availableQty,
    greatest(
      0,
      40-ifnull(
        (select sum(availableQty) from purchases as b where b.id<a.id),
        0
      )
    )
  )
as usedQty
from purchases as a
where item_id = 1) as t
  on u.id = t.id
  set u.availableQty = u.availableQty-t.usedQty;

fiddle: http://sqlfiddle.com/#!9/0a756/1

Iłya Bursov
  • 23,342
  • 4
  • 33
  • 57
  • Thanks for your help. The FIFO system will need to use the purchase_on not the id. Also, the query does not generate the correct cost. Please review the result of my query as it will give the correct output. – Jaylen Jan 14 '15 at 06:23
  • @Mike you can easily use date instead of id, but id is faster and as soon as both are growing - they are the same, cost is 36 which is correct http://sqlfiddle.com/#!9/744f3/3 – Iłya Bursov Jan 14 '15 at 08:52
  • I understand that id "integer" will be faster but the rule that I have to work with is the date. I changed the b.id < a.id to b.purchased_on < a.purchased_on and it worked. But, I am, not 100% clear of the logic that you are using there. What is confusing me is that you are summing the availableQty for all rows where the row is is less than the current row. so if I am on row 10 then sum all the availableQty value in all previous rows. Can you please help me understand the logic? – Jaylen Jan 14 '15 at 16:47
  • Thank you so much. This is pretty clever :) – Jaylen Jan 15 '15 at 19:17