Accounting Calculate Debit credit in SQL(ssms) - sql

I have an accounting calculation problem. I want to write it with SQL Query (in ssms).
I have two groups of documents related to one person (creditor and debtor)
Creditor documents cover debtor documents.
Consider the following example: (How can the result be achieved?)
USE [master]
GO
DROP TABLE IF EXISTS #credit/*creditor=0*/,#debit/*Debtor=1*/
SELECT *
INTO #debit
FROM (values
(88,'2/14',1,5,1),(88,'2/15',2,5,1)
)A (personID,DocDate,DocID,Fee,IsDebit)
SELECT *
INTO #credit
FROM (values
(88,'2/16',3,3,0),(88,'2/17',4,7,0)
)A (personID,DocDate,DocID,Fee,ISDeb)
SELECT * FROM #credit
SELECT * FROM #debit
--result:
;WITH res AS
(
SELECT 88 AS personID ,1 deb_DocID ,5 deb_Fee , 3 Cre_DocID ,3 Cre_Fee, 0 remain_Cre_Fee
UNION
SELECT 88 AS personID ,1 deb_DocID ,5 deb_Fee , 4 Cre_DocID ,7 Cre_Fee, 5 remain_Cre_Fee
UNION
SELECT 88 AS personID ,2 deb_DocID ,5 deb_Fee , 4 Cre_DocID ,7 Cre_Fee, 0 remain_Cre_Fee
)
SELECT *
FROM res

Sample data
Using an ISO date format to avoid any confusion.
The docdate and isdebit columns will not be used in the solution...
I ignored the docdate under the assumptions that the values are incremental and that it is allow to deposit a credit fee before any debit fee.
The isdebit flag seems redundant if you are going to store debit and credit transactions in separate tables anyway.
Updated sample data:
create table debit
(
personid int,
docdate date,
docid int,
fee int,
isdebit bit
);
insert into debit (personid, docdate, docid, fee, isdebit) values
(88, '2021-02-14', 1, 5, 1),
(88, '2021-02-15', 2, 5, 1);
create table credit
(
personid int,
docdate date,
docid int,
fee int,
isdebit bit
);
insert into credit (personid, docdate, docid, fee, isdebit) values
(88, '2021-02-16', 3, 3, 0),
(88, '2021-02-17', 4, 7, 0);
Solution
Couple steps here:
Construct a rolling sum for the debit fees. Done with a first common table expression (cte_debit).
Construct a rolling sum for the credit fees. Done with a second common table expression (cte_credit).
Take all debit info (select * from cte_debit)
Find the first credit info that applies to the current debit info. Done with a first cross apply (cc1). This contains the docid of the first document that applies to the debit document.
Find the last credit info that applies to the current debit info. Done with a second cross apply (cc2). This contains the docid of the last document that applies to the debit document.
Find all credit info that applies to the current debit info by selecting all documents between the first and last applicable document (join cte_credit cc on cc.docid >= cc1.docid and cc.docid <= cc2.docid).
Combine the rolling sum numbers to calculate the remaining credit fees (cc.credit_sum - cd.debit_sum). Use a case expression to filter out negative values.
Full solution:
with cte_debit as
(
select d.personid,
d.docid,
d.fee,
sum(d.fee) over(order by d.docid rows between unbounded preceding and current row) as debit_sum
from debit d
),
cte_credit as
(
select c.personid,
c.docid,
c.fee,
sum(c.fee) over(order by c.docid rows between unbounded preceding and current row) as credit_sum
from credit c
)
select cd.personid,
cd.docid as deb_docid,
cd.fee as deb_fee,
cc.docid as cre_docid,
cc.fee as cre_fee,
case
when cc.credit_sum - cd.debit_sum >= 0
then cc.credit_sum - cd.debit_sum
else 0
end as cre_fee_remaining
from cte_debit cd
cross apply ( select top 1 cc1.docid, cc1.credit_sum
from cte_credit cc1
where cc1.personid = cd.personid
and cc1.credit_sum <= cd.debit_sum
order by cc1.credit_sum desc ) cc1
cross apply ( select top 1 cc2.docid, cc2.credit_sum
from cte_credit cc2
where cc2.personid = cd.personid
and cc2.credit_sum >= cd.debit_sum
order by cc2.credit_sum desc ) cc2
join cte_credit cc
on cc.personid = cd.personid
and cc.docid >= cc1.docid
and cc.docid <= cc2.docid
order by cd.personid,
cd.docid,
cc.docid;
Result
personid deb_docid deb_fee cre_docid cre_fee cre_fee_remaining
-------- --------- ------- --------- ------- -----------------
88 1 5 3 3 0
88 1 5 4 7 5
88 2 5 4 7 0
Fiddle to see things in action. This also contains the intermediate CTE results and some commented helper columns that can be uncommented to help to further understand the solution.

Related

Using cross join with multiple variable in cte

I have some order numbers and want to check if any number has been skipped. I'll use left table method which is:
select * from
#CreatedCrossTable (which has all possibilities)
Left Join #MainTableWhichHaveRealSerialNo mt
where
mt is null
Order no structure is: "CodeType.Year.Month.SerialNo". For instance: "DP.21.07.001","DP.21.07.002".. or not DP, but "FB.21.07.001", "FB.21.07.002" etc.
I want to create a cross table schema for determine the skipped SerilNo values (CreatedCrossTable
above):
(Serial number is reset every month)
CodeType | Year | Month | SerialNo
DP 21 1 1
DP 21 1 2
DP 21 1 3
DP 21 1 4
...
(All SerialNos must increase max serial number of the original table's SerialNo (MainTableWhichHaveRealSerialNo) Also codeType,year and month values must match)
DP 21 2 1
DP 21 2 2
...
FB 21 1 1
FB 21 1 2
...
FB 21 1 1
FB 21 1 2
FB 21 1 3
...
Each Codes' and Month's serial number have a different Maximum Number
for creating CrossTable. I've written that code:
;WITH cteSerialNo AS
(
SELECT 1 AS ORDERNO
UNION ALL
SELECT (ORDERNO+1) AS ORDERNO FROM cteSerialNo WHERE ORDERNO < MAX_ORDER_NO
)
,cteMonthYear AS
(
SELECT CAST('2021.01.01' AS DATE) AS Dt
UNION ALL
SELECT DATEADD(MONTH , 1, Dt) AS Dt
FROM cteMonthYear
WHERE DATEADD (MONTH, 1, Dt) < GETDATE()
)
SELECT
*
FROM
(
SELECT
CODES.CODETYPE,
YEAR(Dts.Dt) AS 'YEAR',
MONTH(Dts.Dt) AS 'MONTH'
FROM
##KK_TBL_CODETYPES AS CODES
CROSS JOIN cteMonthYear AS Dts
) AS CROSSTABLE
CROSS JOIN cteSerialNo AS cSN
How can i enter (MAX_ORDER_NO) for each variable in this code?
Assuming that the max SerialNo value is based on the existing values in the SerialNo column, you would want to just find all possible combinations up to that SerialNo value and then remove those that have a match in the source data:
-- Define test data
declare #t table(CodeType varchar(2),[Year] int,[Month] int,SerialNo int);
insert into #t values
('DP',21,1,1)
,('DP',21,1,2)
,('DP',21,1,3)
--,('DP',21,1,4) -- Missing so should be in Output
,('DP',21,1,5)
,('DP',21,2,1)
,('DP',21,2,2)
,('FB',21,1,1)
,('FB',21,1,2)
,('FB',21,2,1)
,('FB',21,2,2)
--,('FB',21,2,3) -- Missing so should be in Output
,('FB',21,2,4)
;
with m as -- Get Max SerialNo for each grouping
(
select CodeType
,[Year]
,[Month]
,max(SerialNo) as MaxSerialNo
from #t
group by CodeType
,[Year]
,[Month]
)
,t as -- Create a table with 10 rows in
(
select t
from(values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) as t(t)
)
,n as -- Self join those 10 rows 5 times to generate a possible 10*10*10*10*10 = 100,000 incrementing numbers using row_number
(
select top(select max(MaxSerialNo) from m) row_number() over (order by (select null)) as n
from t,t t2,t t3,t t4,t t5
)
-- Join from the numbers table to the source data to generate all possible SerialNo values up to the Max
select m.CodeType
,m.[Year]
,m.[Month]
,n.n as SerialNo
from n
left join m
on n.n <= m.MaxSerialNo
except -- Then exclude any that are in the source data
select CodeType
,[Year]
,[Month]
,SerialNo
from #t
order by CodeType
,[Year]
,[Month]
,SerialNo
Output
CodeType
Year
Month
SerialNo
DP
21
1
4
FB
21
2
3

How to find observations that occurs at least 3 times spanning at least 15 days but no more than 90 days for each unique ID in SQL?

Suppose I have this table:
CREATE TABLE #t1
(
PersonID int ,
ExamDates date,
Score varchar(50) SPARSE NULL,
);
SET dateformat mdy;
INSERT INTO #t1 (PersonID, ExamDates, Score)
VALUES (1, '1.1.2018',70),
(1, '1.13.2018', 100),
(1, '1.18.2018', 85),
(2, '1.1.2018', 90),
(2, '2.1.2018', 95),
(2, '3.15.2018', 95),
(2, '7.30.2018', 100),
(3, '1.1.2018', 80),
(3, '1.2.2018', 80),
(3, '5.3.2018', 50),
(4, '2.1.2018', 90),
(4, '2.20.2018', 100);
I would like to find observations that occurs at least 3 times spanning at least 15 days but no more than 90 days for each unique ID.
My final table should look like this:
PersonID
ExamDates
Score
1
1/1/2018
70
1
1/13/2018
100
1
1/18/2018
85
2
1/1/2018
90
2
2/1/2018
95
2
3/15/2018
95
We have code working for this using R, but would like to avoid pulling large datasets into R just to run this code. We are doing this in a very large dataset and concerned about efficiency of the query.
Thanks!
-Peter
To start with, the common name for this situation is Gaps and Islands. That will help you as you search for answers or come up with similar problems in the future.
That out of the way, here is my solution. Start with this:
WITH Leads As (
SELECT t1.*
, datediff(day, ExamDates, lead(ExamDates, 2, NULL) over (partition by PersonID ORDER BY ExamDates)) As Diff
FROM t1
)
SELECT *
FROM Leads
WHERE Diff BETWEEN 15 AND 90
I have to use the CTE, because you can't put a windowing function in a WHERE clause. It produces this result, which is only part of what you want:
PersonID
ExamDates
Score
Diff
1
2018-01-01
70
17
2
2018-01-01
90
73
This shows the first record in each group. We can use it to join back to the original table and find all the records that meet the requirements.
But first, we have a problem. The sample data only has groups with exactly three records. However, the real data might end up with groups with more than three items. In that case this would find multiple first records from the same group.
You can see it in this updated SQL Fiddle, which adds an additional record for PersonID #1 that is still inside the date range.
PersonID
ExamDates
Score
Diff
1
2018-01-01
70
17
1
2018-01-13
100
29
2
2018-01-01
90
73
I'll be using this additional record in every step from now on.
To account for this, we also need to check to see each record is not in the middle or end of a valid group. That is, also look a couple records both ahead and behind.
WITH Diffs As (
SELECT #t1.*
, datediff(day, ExamDates, lead(ExamDates, 2, NULL) over (partition by PersonID ORDER BY ExamDates)) As LeadDiff2
, datediff(day, ExamDates, lead(ExamDates, 2, NULL) over (partition by PersonID ORDER BY ExamDates)) As LeadDiff1
, datediff(day, lag(ExamDates, 1, NULL) over (partition by PersonID ORDER BY ExamDates), ExamDates) as LagDiff1
, datediff(day, lag(ExamDates, 2, NULL) over (partition by PersonID ORDER BY ExamDates), ExamDates) as LagDiff2
FROM #t1
)
SELECT *
FROM Diffs
WHERE LeadDiff2 BETWEEN 15 AND 90
AND coalesce(LeadDiff1 + LagDiff1,100) > 90 /* Not in the middle of a valid group */
AND coalesce(Lagdiff2, 100) > 90 /* Not at the end of a valid group */
This code gets us back to the original results, even with the additional record. Here's the updated fiddle:
http://sqlfiddle.com/#!18/ea12ad/23
Now we can join back to the original table and find all records in each group:
WITH Diffs As (
SELECT 3t1.*
, datediff(day, ExamDates, lead(ExamDates, 2, NULL) over (partition by PersonID ORDER BY ExamDates)) As LeadDiff2
, datediff(day, ExamDates, lead(ExamDates, 2, NULL) over (partition by PersonID ORDER BY ExamDates)) As LeadDiff1
, datediff(day, lag(ExamDates, 1, NULL) over (partition by PersonID ORDER BY ExamDates), ExamDates) as LagDiff1
, datediff(day, lag(ExamDates, 2, NULL) over (partition by PersonID ORDER BY ExamDates), ExamDates) as LagDiff2
FROM #t1
), FirstRecords AS (
SELECT PersonID, ExamDates, DATEADD(day, 90, ExamDates) AS FinalDate
FROM Diffs
WHERE LeadDiff2 BETWEEN 15 AND 90
AND coalesce(LeadDiff1 + LagDiff1,100) > 90 /* Not in the middle of a valid group */
AND coalesce(lagdiff2, 100) > 90 /* Not at the end of a valid group */
)
SELECT t.*
FROM FirstRecords f
INNER JOIN #t1 t ON t.PersonID = f.PersonID
AND t.ExamDates >= f.ExamDates
AND t.ExamDates <= f.FinalDate
ORDER BY t.PersonID, t.ExamDates
That gives me this, which matches your desired output and my extra record:
PersonID
ExamDates
Score
1
2018-01-01
70
1
2018-01-13
100
1
2018-01-18
85
1
2018-02-11
89
2
2018-01-01
90
2
2018-02-01
95
2
2018-03-15
95
See it work here:
http://sqlfiddle.com/#!18/ea12ad/26
Here's Eli's idea done a bit more simply, and moving all of the heavy computation to the cte, where it may possibly be more efficient:
With cte As (
Select PersonID, ExamDates
,Case When Datediff(DAY,ExamDates, Lead(ExamDates,2,Null) Over (Partition by PersonID Order by ExamDates)) Between 15 and 90
Then Lead(ExamDates,2,Null) Over (Partition by PersonID Order by ExamDates)
Else NULL End as EndDateRange
From #t1
)
Select Distinct B.*
From cte Inner Join #t1 B On B.PersonID=cte.PersonID
And B.ExamDates Between cte.ExamDates and cte.EndDateRange
The Case statement in the CTE only returns a valid date if the entry two items later satisfies the overall condition; that date is used to form a range with the current record's ExamDate. By returning NULL on non-qualified ranges we ensure the join in the outer part of the SQL is not satisfied. The Distinct clause is needed to collapse duplicates when there are are 4+ consecutive observations within the 15-90 day range.
You'll need a CTE to identify the base for the conditions which you described.
This code works with your sample set, and should work even when you have a larger set - though may require a distinct if you have overlapping results, i.e. 5 exam dates in the 15-90 range.
WITH cte AS(
SELECT
PERSONID
,EXAMDATES
,Score
,COUNT(*) OVER (PARTITION BY PERSONID ORDER BY ExamDates ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW )AS COUNTS
,LAG(ExamDates,2,NULL) OVER (PARTITION BY PERSONID ORDER BY ExamDates) DIFFS
FROM #t1
)
SELECT B.*
FROM CTE
INNER JOIN #T1 B ON CTE.PERSONID = B.PERSONID
WHERE CTE.COUNTS >=3
AND DATEDIFF(DAY,CTE.DIFFS,CTE.EXAMDATES) BETWEEN 15 AND 90
AND B.EXAMDATES BETWEEN CTE.DIFFS AND CTE.EXAMDATES

Alternative: Sql - SELECT rows until the sum of a row is a certain value

My question is very similar to my previous one posted here:
Sql - SELECT rows until the sum of a row is a certain value
To sum it up, I need to return the rows, until a certain sum is reached, but the difference this time, is that, I need to find the best fit for this sum, I mean, It doesn't have to be sequential. For example:
Let's say I have 5 unpaid receipts from customer 1:
Receipt_id: 1 | Amount: 110€
Receipt_id: 2 | Amount: 110€
Receipt_id: 3 | Amount: 130€
Receipt_id: 4 | Amount: 110€
Receipt_id: 5 | Amount: 190€
So, customer 1 ought to pay me 220€.
Now I need to select the receipts, until this 220€ sum is met and it might be in a straight order, like (receipt 1 + receipt 2) or not in a specific order, like (receipt 1 + receipt 4), any of these situations would be suitable.
I am using SQL Server 2016.
Any additional questions, feel free to ask.
Thanks in advance for all your help.
This query should solve it.
It is a quite dangerous query (containing a recursive CTE), so please be careful!
You can find some documentation here: https://www.essentialsql.com/recursive-ctes-explained/
WITH the_data as (
SELECT *
FROM (
VALUES (1, 1, 110),(1, 2,110),(1, 3,130),(1, 4,110),(1, 5,190),
(2, 1, 10),(2, 2,20),(2, 3,200),(2, 4,190)
) t (user_id, receipt_id, amount)
), permutation /* recursive used here */ as (
SELECT
user_id,
amount as sum_amount,
CAST(receipt_id as varchar(max)) as visited_receipt_id,
receipt_id as max_receipt_id,
1 as i
FROM the_data
WHERE amount > 0 -- remove empty amount
UNION ALL
SELECT
the_data.user_id,
sum_amount + amount as sum_amount,
CAST(concat(visited_receipt_id, ',', CAST(receipt_id as varchar))as varchar(max)) as visited_receipt_id,
receipt_id as max_receipt_id ,
i + 1
FROM the_data
JOIN permutation
ON the_data.user_id = permutation.user_id
WHERE i < 1000 -- max 1000 loops, means any permutation with less than 1000 different receipts
and receipt_id > max_receipt_id -- in order that sum in komutatif , we can check the sum in any unique order ( here we take the order of the reciept_id in fact we do not produce any duplicates )
-- AND sum_amount + amount <= 220 -- ignore everything that is bigger than the expected value (optional)
)
SELECT *
FROM permutation
WHERE sum_amount = 220
in order to select only one combination per user_id, replace the last three lines of the previous query by
SELECT *
FROM (
SELECT *, row_number() OVER (partition by user_id order by random() ) as r
FROM permutation
WHERE sum_amount = 220
) as t
WHERE r = 1
IF your target is to sum only 2 receipts in order to reach your value, this could be a solution:
DECLARE #TARGET INT = 220 --SET YOUR TARGET
, #DIFF INT
, #FIRSTVAL INT
SET #FIRSTVAL = (
SELECT TOP 1 AMOUNT
FROM myRECEIPTS
ORDER BY RECEIPT_ID ASC
)
SELECT TOP 1 *
FROM myRECEIPTS
WHERE AMOUNT = #TARGET - #FIRSTVAL
ORDER BY RECEIPT_ID ASC
this code will do it:
declare #sum1 int
declare #numrows int
set #numrows= 1
set #sum1 =0
while (#sum1 < 10)
begin
select top (#numrows) #sum1=sum(sum1) from receipts
set #numrows +=1
end
select top(#numrows) * from receipts

SQL Group By Certain Amounts

I have an order table which has the customer id and order amount. I want to join these orders but joined orders cannot exceed a certain amount. An example below:
Let's say the maximum amount is 33 pallets and I have a table like this:
Order ID Client ID Amount
1 100001 10
2 100001 22
3 100001 13
4 100001 33
5 100001 1
6 100001 5
7 100001 6
The result should be:
Order ID Client ID Amount Joined ID Joined Amount
1 100001 10 100001A 32
2 100001 22 100001A 32
3 100001 13 100001B 13
4 100001 33 100001C 33
5 100001 1 100001D 12
6 100001 5 100001D 12
7 100001 6 100001D 12
Here, if we can also come up with a way to ad orders numbered 5,6,7 to joined order 10001B it would be great. But even this solution will be enough.
I have a few ideas on how to solve this but I couldn't really come up with a working solution. I'll be handling around 2000 Order Ids like this, so also I don't want this to be a slow operation. I'm using SQL Server 2014
you can find proposed solution (sql definition) with help of recursive CTE here: http://sqlfiddle.com/#!6/285c16/45
basicaly CTE iterates ordered list (by clientID, orderID) and evaluate if summed amount is not over 33.
i have added next clientID to mock data, to test correct subcount criteria evaluation.
here is query to obtain results:
-- prepare numbering for iteration
with orders_nr
as
(
select row_number() over(order by clientID, id) as [nr],
o.*
from orders o
)
,
-- prepare sum totals
re
as
(
select id, amount, amount as amount_total ,o.[nr] as nr,
clientID
from orders_nr o
where o.[nr]=1
UNION ALL
select o.id, o.amount,
CASE WHEN o.clientID <> r.clientID then o.amount
ELSE o.amount+ r.amount_total END,
o.[nr] as nr, o.clientID
from orders_nr o join re r
on (o.[nr]=r.[nr]+1)
)
,
-- iterate total - evaluate current criteria (<=33)
re2 as
(
select re.id, re.amount, re.amount_total,
re.[nr] as [group], re.[nr], re.clientID
from re
where re.[nr]=1
UNION ALL
select r.id, r.amount,
CASE WHEN r.amount+re2.amount_total >33
OR r.clientID<>re2.clientID
then r.amount ELSE re2.amount_total+r.amount END
as amount_total,
CASE WHEN r.amount+re2.amount_total >33
OR r.clientID<>re2.clientID THEN
r.[nr] ELSE re2.[group] END as [group], r.[nr], r.clientID
from re r join re2
on (r.[nr]=re2.[nr]+1 )
)
, group_total
AS
(
select [group], clientID, max(amount_total) as total
FROM re2
group by [group], clientID
),
result
as
(
select
r.id, r.clientID, r.amount,
cast(r.clientid as varchar(20))
+'-'+char(64+cast(
dense_rank()
over( partition by r.clientID
order by r.[clientID], r.[group])
as varchar(3))) as joinedID
, gt.total as joinedAmount
from re2 as r join group_total gt
on (r.clientID=gt.clientID AND r.[group]=gt.[group])
)
select * from result
Not certain if I'm understanding the question correctly, but you might try
select [Client ID], [Joined ID], sum([Amount]) as Total_Amount
from [table_name]
group by [Client ID], [Joined ID]
having sum([Amount]) <= 33
It leaves off the Order ID, but since it looks to be unique, you can't use it in a group by.
Edit was to add the having clause in the query to say we can't have something that adds up to more than 33.
I tried to solve with simple selects and without using an explicit cursor but it was a little hard in that way.
I've solved it and got exactly what you wanted with:
a TempTable, a cursor, a counter for checking the sum of sequent amounts, CHAR() function to generate letters; I calculated the values and inserted into temp table finally updated the temp table, following is what I tried and the DEMO IS HERE.
create table #tbl_name
(OrderID int,
ClientID int,
Amount int,
joinedId varchar(15) ,
joinedAmount int)
insert #tbl_name(OrderID,ClientID,Amount)
select OrderID,ClientID,Amount from tbl_name
declare cr cursor for
select orderId,
clientId,
amount
from tbl_name
order by OrderId
declare #summedAmount int,
#orderId int,
#clientId int,
#amount int,
#counter int
set #summedAmount=0
set #counter=65
open cr
fetch from cr into #orderId,#clientId,#amount
while (##fetch_status=0)
begin
if (#amount + #summedAmount < 33)
begin
set #summedAmount=#summedAmount+#amount
update #tbl_name
set joinedId=cast(#ClientId as varchar(10))+char(#counter),
joinedAmount=#summedAmount
where orderId=#orderId
end
else if (#amount + #summedAmount >33)
begin
set #counter=#counter+1
set #summedAmount=#amount
update #tbl_name
set joinedId=cast(#ClientId as varchar(10))+char(#counter),
joinedAmount=#Amount
where orderId=#orderId
end
fetch from cr into #orderId,#clientId,#amount
end
close cr
deallocate cr
go
with CTE as
(
select JoinedId, max(joinedAmount) mx
from #tbl_name
group by JoinedId
)
update #tbl_name
set joinedAmount = CTE.mx
from #tbl_name
join CTE on #tbl_name.JoinedId=CTE.JoinedId
select * from #tbl_name
drop table #tbl_name

How can I spread a value across multiple rows without a cursor?

Is there an easy way to spread a value across multiple rows?
For example, my table contains
Type Invoiced Paid Current
Charge 100 0 100
Charge 100 0 100
Charge 100 0 100
Payment 0 250 0
Payment 0 25 0
The data is imported this way, but I need to populate the Current and Paid columns with whatever they should be for that transaction based on the payment transactions that were also imported.
Is there an easy way to write a query to determine the balance for the Current column for each record?
For example, the 250 would apply 100 for the first two records and 50 to the next two, and the 25 would get applied to the last one, so the end result after updating the Current balance in my table should be:
Type Invoiced Paid Current
Charge 100 100 0
Charge 100 100 0
Charge 100 75 25
Payment 0 250 0
Payment 0 25 0
I'd ideally like to do this with a single query instead of using a cursor to process each item individually. I've been trying to do it by using the Row_Number() function and joining two subqueries, but I know I'm missing something here
Here was my first attempt, which resulted in getting the running total of the current balance
;with cte(invoiced, paid, current)
as (
select invoiced, paid, current
, row_number() over (order by datecreated)
from mytable
)
select t1.invoiced, t1.paid, sum(t2.invoiced - t2.paid) as [current]
from cte as t1
join cte as t2 on t1.number = t2.number and t2.rownum <= t1.rownum
group by t1.uid, t1.number, t1.rownum
order by t1.rownum
Result:
Invoiced Paid Current
100 0 100
100 0 200
100 0 300
0 250 50
0 25 25
I'm sure there's a way to do this, but right now my brain seems on strike and is refusing to come up with a solution.
I think I found a solution
First off, I don't need to link the Paid transactions to the Invoiced transactions, so I only need the sum of all payments
select accountid, sum(paid)
from mytable
where type = 'Payment'
group by accountid
Then I need to apply this value to each record until the running total becomes greater than the total paid.
To do this, I modified my running total query so it only sums the charges instead of summing both charges and payments
;with cte(id, accountid, invoiced, paid, current)
as (
select id, accountid, invoiced, paid, current
, row_number() over (order by datecreated)
from mytable
where type = 'Charge'
)
select t1.id, t1.accountid, t1.invoiced, sum(t2.invoiced) as [runningTotalOfCharges]
from cte as t1
join cte as t2 on t1.number = t2.number and t2.rownum <= t1.rownum
group by t1.id, t1.accountid, t1.invoiced
and joined that to the payment query, so now I have a bunch of rows containing the total payment amount, the running total of charges up until that record, and the current record's charge amount.
From there, I just needed a CASE statement to determine if the charge was fully paid, partially paid, or not paid at all, and use a little math to figure out the Paid and Current records
select charged.Id, charged.AccountId, charged.Invoiced
-- Use Case statements to determine if this payment is fully paid, partially paid,
-- or not paid at all, then determine Current and Paid based on that
, case when totalpaid - runningtotal >= 0 then invoiced
when invoiced > abs(totalpaid - runningtotal) then invoiced + totalpaid - runningtotal
else 0 end as [Paid]
, case when totalpaid - runningtotal >= 0 then 0
when invoiced > abs(totalpaid - runningtotal) then abs(totalpaid - runningtotal)
else invoiced end as [Current]
from
(
-- Running total query from above
select t1.id, t1.accountid, t1.invoiced, sum(t2.invoiced) as [runningtotal]
from cte as t1
join cte as t2 on t1.number = t2.number and t2.rownum <= t1.rownum
group by t1.id, t1.accountid, t1.invoiced
) as charged
inner join (
-- Total Paid query from above
select accountid, sum(paid) as totalpaid
from mytable
where type = 'Payment'
group by accountid
) as paid on charged.number = paid.number
And the end result is what I want. Just need to join this to the actual data table via the Id column, and update the Paid and Current values :)
Id AccountId Invoiced Paid Current
1 1 100 100 0
2 1 100 100 0
3 1 100 75 25
You could calculate running totals and sums for the paid and current columns and then perform some math to get the current column values. I gave it a try but got bogged down in the math to come up with the final values. It's not an easy way, but it's a way.
DECLARE #myTable TABLE
(
[TranId] INT,
[Type] VARCHAR(10),
[Invoiced] INT,
[Paid] INT,
[Current] INT,
[SumPaid] INT,
[SumCurrent] INT,
[RunningPaid] INT,
[RunningCurrent] INT
)
INSERT INTO #myTable SELECT 1, 'Charge', 100, 0, 100, null, null, null, null
INSERT INTO #myTable SELECT 2, 'Charge', 100, 0, 100, null, null, null, null
INSERT INTO #myTable SELECT 3, 'Charge', 100, 0, 100, null, null, null, null
INSERT INTO #myTable SELECT 4, 'Paid', 0, 250, 0, null, null, null, null
INSERT INTO #myTable SELECT 5, 'Paid', 0, 25, 0, null, null, null, null
UPDATE #myTable SET SumPaid = (SELECT SUM([Paid]) FROM #myTable)
UPDATE #myTable SET SumCurrent = (SELECT SUM([Current]) FROM #myTable)
UPDATE #myTable
SET
[RunningPaid] = full_running_total_set.[RunningPaid],
[RunningCurrent] = full_running_total_set.[RunningCurrent]
FROM #myTable
INNER JOIN
(
SELECT
TranId1,
SUM([Paid]) AS [RunningPaid],
SUM([Current]) AS [RunningCurrent]
FROM
(
SELECT
set2.[Paid],
set2.[Current],
set1.[TranId] AS [TranId1],
set2.[TranId] AS [TranId2]
FROM #myTable set1
INNER JOIN #myTable set2
ON set1.[TranId] >= set2.[TranId]
)running_total_set
GROUP BY [TranId1]
)full_running_total_set
ON [TranId] = full_running_total_set.[TranId1]
SELECT * FROM #myTable