I'm writing a small budget app for myself, and am a bit stuck with a query.
It's a query to calculate the monthly budget totals, per month, per category.
On each month, you can set a rule against the category to either affect the main budget buffer, or be confined to that category only.
How it works is:
- If it affects buffer, any money you overspend, resets each month for that category.
- If it affects buffer, any money you save, is stored against that category for later use.
If it's confined, then it always (even if you overspend) keeps counting it in the next month, until you set it back to affects buffer.
Note: The last negative amount should be counted in the first occurance of affectsbuffer, so that you can pay off any negative amount for that month. So the negative stops getting carried over AFTER the first affects buffer month.
Here is my sample data, along with a Balance Should Be column to show how it is meant to work.
CREATE TABLE [BGT].[BudgetTemp2] ( [Month] date, [CategoryID] int, [Budgeted] money, [Outflows] money, [BudgetedAndOutflows] money, [OverspendingHandling] nvarchar(50), [BalanceShouldBe] money )
INSERT INTO [BGT].[BudgetTemp2]
VALUES
( N'2016-01-01T00:00:00', 116, 0.0000, -500.0000, -500.0000, N'AffectsBuffer', -500.0000 ),
( N'2016-02-01T00:00:00', 116, 50.0000, -200.0000, -150.0000, N'AffectsBuffer', -150.0000 ),
( N'2016-03-01T00:00:00', 116, 50.0000, 0.0000, 50.0000, N'AffectsBuffer', 50.0000 ),
( N'2016-04-01T00:00:00', 116, 0.0000, -350.0000, -350.0000, N'AffectsBuffer', -300.0000 ),
( N'2016-05-01T00:00:00', 116, 100.0000, 0.0000, 100.0000, N'AffectsBuffer', 100.0000 ),
( N'2016-06-01T00:00:00', 116, 0.0000, 10699.8900, 10699.8900, N'AffectsBuffer', 10799.8900 ),
( N'2016-07-01T00:00:00', 116, 4147.8800, -16707.6900, -12559.8100, N'Confined', -1759.9200 ),
( N'2016-08-01T00:00:00', 116, 0.0000, -4504.9600, -4504.9600, N'Confined', -6264.8800 ),
( N'2016-09-01T00:00:00', 116, 0.0000, -5486.5400, -5486.5400, N'Confined', -11751.4200 ),
( N'2016-10-01T00:00:00', 116, 0.0000, -3795.7700, -3795.7700, N'Confined', -15547.1900 ),
( N'2016-11-01T00:00:00', 116, 0.0000, 407.3200, 407.3200, N'Confined', -15139.8700 ),
( N'2016-12-01T00:00:00', 116, 298324.5900, -282434.7200, 15889.8700, N'Confined', 750.0000 ),
( N'2017-01-01T00:00:00', 116, 4196.4400, -4196.4400, 0.0000, N'Confined', 750.0000 ),
( N'2017-02-01T00:00:00', 116, 10999.9000, -15199.9000, -4200.0000, N'Confined', -3450.0000 ),
( N'2017-03-01T00:00:00', 116, 4987.6600, -2875.1800, 2112.4800, N'Confined', -1337.5200 ),
( N'2017-04-01T00:00:00', 116, 4899.1600, -65100.0000, -60200.8400, N'Confined', -61538.3600 ),
( N'2017-05-01T00:00:00', 116, 504.3200, 0.0000, 504.3200, N'Confined', -61034.0400 ),
( N'2017-06-01T00:00:00', 116, 0.0000, -104505.0300, -104505.0300, N'Confined', -165539.0700 ),
( N'2017-07-01T00:00:00', 116, 0.0000, -72317.7100, -72317.7100, N'Confined', -237856.7800 ),
( N'2017-08-01T00:00:00', 116, 0.0000, -82.2200, -82.2200, N'Confined', -237939.0000 ),
( N'2017-09-01T00:00:00', 116, 237916.0900, -814.4600, 237101.6300, N'Confined', -837.3700 ),
( N'2017-10-01T00:00:00', 116, 906.8300, -1523.5500, -616.7200, N'Confined', -1454.0900 ),
( N'2017-11-01T00:00:00', 116, 175.6100, -3348.5500, -3172.9400, N'Confined', -4627.0300 ),
( N'2017-12-01T00:00:00', 116, -14.4400, -1763.4400, -1777.8800, N'AffectsBuffer', -6404.9100 ),
( N'2018-01-01T00:00:00', 116, 40.0000, -20.0000, 20.0000, N'AffectsBuffer', 20.0000 ),
( N'2018-02-01T00:00:00', 116, 0.0000, -75.4300, -75.4300, N'AffectsBuffer', -55.4300 ),
( N'2018-04-01T00:00:00', 116, 4899.7400, -4899.7400, 0.0000, N'AffectsBuffer', 0.0000 ),
( N'2018-05-01T00:00:00', 116, 750.3900, -750.3900, 0.0000, N'AffectsBuffer', 0.0000 ),
( N'2018-06-01T00:00:00', 116, 0.0000, -500.0000, -500.0000, N'Confined', -500.0000 ),
( N'2018-07-01T00:00:00', 116, 100.0000, 0.0000, 0.0000, N'Confined', -400.0000 ),
( N'2018-08-01T00:00:00', 116, 200.0000, -100.0000, 100.0000, N'Confined', -300.0000 ),
( N'2018-09-01T00:00:00', 116, 0.0000, 0.0000, 0.0000, N'AffectsBuffer', -300.0000 ),
( N'2018-10-01T00:00:00', 116, 100.0000, -50.0000, 50.0000, N'AffectsBuffer', 50.0000 ),
( N'2018-11-01T00:00:00', 116, 0.0000, -500.0000, -500.0000, N'AffectsBuffer', -450.0000 ),
( N'2018-12-01T00:00:00', 116, 100.0000, -50.0000, 50.0000, N'AffectsBuffer', 50.0000 ),
( N'2019-01-01T00:00:00', 116, 0.0000, 0.0000, 0.0000, N'AffectsBuffer', 50.0000 ),
( N'2019-02-01T00:00:00', 116, 100.0000, 0.0000, 100.0000, N'Confined', 150.0000 ),
( N'2019-03-01T00:00:00', 116, 0.0000, -50.0000, -50.0000, N'Confined', 100.0000 ),
( N'2019-04-01T00:00:00', 116, 0.0000, -200.0000, -200.0000, N'Confined', -100.0000 ),
( N'2019-05-01T00:00:00', 116, 0.0000, -200.0000, -200.0000, N'Confined', -300.0000 ),
( N'2019-06-01T00:00:00', 116, 0.0000, 0.0000, 0.0000, N'AffectsBuffer', -300.0000 ),
( N'2019-07-01T00:00:00', 116, 0.0000, 0.0000, 0.0000, N'AffectsBuffer', 0.0000 )
--DROP TABLE [BGT].[BudgetTemp2]
Here is my query so far, but as you may see, it ended up in a bit of a mess. I feel that I have everything in here to be able to do it correctly, I'm just missing a few key things. It's counting correctly until I have the changes between Affects Buffer, and Confined.
-- FROM: https://stackoverflow.com/a/23020788
;WITH c1 AS (
SELECT
*,
LAG([BudgetTemp].[OverspendingHandling], 1, [BudgetTemp].[OverspendingHandling])
OVER (PARTITION BY [BudgetTemp].[CategoryID] ORDER BY [BudgetTemp].[Month]) AS PrevOverspendingHandling
FROM [BGT].[BudgetTemp2] [BudgetTemp]
), c2 AS (
SELECT
*,
SUM(CASE WHEN [c1].[OverspendingHandling] <> [c1].[PrevOverspendingHandling] THEN 1 ELSE 0 END)
OVER (PARTITION BY [c1].[CategoryID] ORDER BY [c1].[Month]) AS Ranker
FROM [c1]
), c3 AS (
SELECT
*,
SUM([c2].[BudgetedAndOutflows]) OVER (PARTITION BY [c2].[CategoryID] ORDER BY [c2].[Month] ROWS UNBOUNDED PRECEDING) AS rt,
SUM([c2].[BudgetedAndOutflows]) OVER (PARTITION BY [c2].[CategoryID], [c2].[Ranker] ORDER BY [c2].[Month] ROWS UNBOUNDED PRECEDING) AS rt2
FROM [c2]
), c4 AS (
SELECT
*,
MIN(rt) OVER (PARTITION BY [c3].[CategoryID] ORDER BY [c3].[Month] ROWS UNBOUNDED PRECEDING) AS rt_min,
MIN(rt) OVER (PARTITION BY [c3].[CategoryID], [c3].[Ranker] ORDER BY [c3].[Month] ROWS UNBOUNDED PRECEDING) AS rt2_min
FROM [c3]
), c5 AS (
SELECT
*,
-- WE WANT TO LAG MIN_CUR BY 1 SO THAT WE STILL GET A SINGLE NEGATIVE FOR THAT MONTH, BUT IT RESETS THE NEXT MONTH
LAG(rt_min, 1, 0) OVER (PARTITION BY [c4].[CategoryID] ORDER BY [c4].[Month]) AS rt_min_lag,
LAG(rt2_min, 1, 0) OVER (PARTITION BY [c4].[CategoryID], [c4].[Ranker] ORDER BY [c4].[Month]) AS rt2_min_lag
FROM [c4]
)
SELECT
[Month],
[CategoryID],
[Budgeted],
[Outflows],
[BudgetedAndOutflows],
[OverspendingHandling],
[PrevOverspendingHandling],
[Ranker],
[rt],
[rt_min],
[rt_min_lag],
[rt2],
[rt2_min],
[rt2_min_lag],
[rt] + (CASE WHEN [rt_min_lag] < 0 THEN -[rt_min_lag] ELSE 0 END) AS Balance1,
[rt2] + (CASE WHEN [rt2_min_lag] < 0 THEN -[rt2_min_lag] ELSE 0 END) AS Balance2,
[BalanceShouldBe]
FROM [c5]
ORDER BY
[CategoryID],
[Month]
Any help would be greatly appreciated!
Cheers,
EDIT: @dfundako found a couple of incorrect data entries, and I have updated the following:
- 2018-01-01 Updated BalanceShouldBe FROM -6384.91 TO 20.00
- 2018-02-01 Updated BalanceShouldBe FROM -75.43 TO -55.43
I have updated this in the main insert script also.