Invoices with concern dates that overide the issue date

I have a table with Invoice Documents per Project, in which i need to get statistics per project , year , month with sql. The difficulty is that for some of the invoices the user will have entered concerned dates that must be override the issued month of the Invoice Document.

Ī™ am really stuggling how this can be done. Do you think it is a must for this to use a Stored Procedure for the result .. Any help is welcome because i am really struggling to get the desired result :slight_smile:

Consumer data is provided

SET ANSI_NULLS, QUOTED_IDENTIFIER, ANSI_PADDING ON;
GO
CREATE TABLE #Invoices
(
Project Varchar(10) NOT NULL
,DocumentCode Varchar(15) NOT NULL
,IssueDate Date NOT NULL
,Cost Decimal NOT NULL
,ConcernFrom Date NULL
,ConcernTo Date NULL

);
INSERT INTO #Invoices
VALUES ('Prj-001', 'DOC-50001', '20230601', 500 , NULL , NULL),
('Prj-001', 'DOC-50002', '20230615', 1000 , '20230615' , '20230720'),
('Prj-001', 'DOC-50003', '20230625', 300 , NULL , NULL),
('Prj-002', 'DOC-50004', '20230710', 250 , NULL , NULL),
('Prj-002', 'DOC-50005', '20230731', 100 , NULL , NULL),
('Prj-002', 'DOC-50006', '20231015', 500 , '20231215' , '20240131')

Thank you for the quality of your answer, it's very clear what you want. A stored procedure is not needed but it's very complex. As you have spend a lot of time to ask your question I will spend a lot of time to answer your question.

First we have to understand that we need 2 querys as we have two situations, 1 is with a concern date and 1 is without. The last one is simple.

I've adjusted your example a little bit as concernfrom can contain a period with more then 2 months. I'm not sure it will but if it's happening your query isn't broken :wink:

That is why I created a YearMonth table to contain the years and months involved with a technique called CTE, commen table expression.

;WITH Years AS
(
SELECT 2022 AS [Year]
UNION
SELECT 2023 AS [Year]
UNION
SELECT 2024 AS [Year]
), Months AS
(
SELECT 1 AS [Month]
UNION
SELECT 2 AS [Month]
UNION
SELECT 3 AS [Month]
UNION
SELECT 4 AS [Month]
UNION
SELECT 5 AS [Month]
UNION
SELECT 6 AS [Month]
UNION
SELECT 7 AS [Month]
UNION
SELECT 8 AS [Month]
UNION
SELECT 9 AS [Month]
UNION
SELECT 10 AS [Month]
UNION
SELECT 11 AS [Month]
UNION
SELECT 12 AS [Month]
), InvoicePeriod AS
(
SELECT Years.[Year], Months.[Month], Years.[Year] *100 + Months.[Month] AS [InvoicePeriod], DATEFROMPARTS(Years.Year,Months.Month,1) AS BeginOfMonth, EOMONTH(DATEFROMPARTS(Years.Year,Months.Month,1)) EndOfMonth
FROM Years
CROSS JOIN Months
) SELECT * FROM InvoicePeriod;

As you can see we have a table with fields that helps us to calculate the results. At the end we combine 2 query results with UNION so we have 1 result for 2 situations.

This is the final result, it's not perfect as the number of days is not the same as your example but you'll get the idea:

SET ANSI_NULLS, QUOTED_IDENTIFIER, ANSI_PADDING ON;
GO

DROP TABLE IF EXISTS #Invoices;

CREATE TABLE #Invoices
(
Project Varchar(10) NOT NULL
,DocumentCode Varchar(15) NOT NULL
,IssueDate Date NOT NULL
,Cost Decimal(8,2) NOT NULL
,ConcernFrom Date NULL
,ConcernTo Date NULL

);
INSERT INTO #Invoices
VALUES ('Prj-001', 'DOC-50001', '20230601', 500 , NULL , NULL),
('Prj-001', 'DOC-50002', '20230615', 1000 , '20230615' , '20230720'),
('Prj-001', 'DOC-50003', '20230625', 300 , NULL , NULL),
('Prj-002', 'DOC-50004', '20230710', 250 , NULL , NULL),
('Prj-002', 'DOC-50005', '20230731', 100 , NULL , NULL),
('Prj-002', 'DOC-50006', '20231015', 500 , '20231215' , '20240131')

;WITH Years AS
(
SELECT 2022 AS [Year]
UNION
SELECT 2023 AS [Year]
UNION
SELECT 2024 AS [Year]
), Months AS
(
SELECT 1 AS [Month]
UNION
SELECT 2 AS [Month]
UNION
SELECT 3 AS [Month]
UNION
SELECT 4 AS [Month]
UNION
SELECT 5 AS [Month]
UNION
SELECT 6 AS [Month]
UNION
SELECT 7 AS [Month]
UNION
SELECT 8 AS [Month]
UNION
SELECT 9 AS [Month]
UNION
SELECT 10 AS [Month]
UNION
SELECT 11 AS [Month]
UNION
SELECT 12 AS [Month]
), InvoicePeriod AS
(
SELECT
Years.[Year],
Months.[Month], Years.[Year] *100 + Months.[Month] AS [InvoicePeriod],
DATEFROMPARTS(Years.Year,Months.Month,1) AS BeginOfMonth,
EOMONTH(DATEFROMPARTS(Years.Year,Months.Month,1)) EndOfMonth
FROM Years
CROSS JOIN Months
), InvoiceTable AS
(
SELECT
,
CASE WHEN ConcernFrom > BeginOfMonth THEN ConcernFrom ELSE BeginOfMonth END AS StartDate,
CASE WHEN ConcernTo < EndOfMonth THEN ConcernTo ELSE EndOfMonth END AS EndDate,
DATEDIFF(DAY,ConcernFrom,ConcernTo) AS TotalDays,
DATEDIFF(DAY,CASE WHEN ConcernFrom > BeginOfMonth THEN ConcernFrom ELSE BeginOfMonth END,CASE WHEN ConcernTo < EndOfMonth THEN ConcernTo ELSE EndOfMonth END) DaysThisPeriod
FROM #Invoices
INNER JOIN InvoicePeriod ON
InvoicePeriod.[InvoicePeriod] BETWEEN YEAR(ConcernFrom)100 + Month(ConcernFrom) AND YEAR(ConcernTo)100 + Month(ConcernTo)
WHERE ConcernFrom IS NOT NULL
)
SELECT
tbl.Project, tbl.[Year], tbl.[Month], SUM(tbl.Cost)
FROM
(
SELECT I.Project, I.[Year], I.[Month], SUM((I.DaysThisPeriod
1.0/I.TotalDays
1.0)
[Cost]) AS Cost
FROM InvoiceTable I
GROUP BY I.Project,I.[Year], I.[Month]
UNION
SELECT
I.Project, YEAR(I.IssueDate), MONTH(I.IssueDate), SUM(Cost) AS Cost
FROM #Invoices I
WHERE ConcernFrom IS NULL
GROUP BY I.Project, YEAR(I.IssueDate), MONTH(I.IssueDate)
) tbl
GROUP BY Project, [Year], [Month];

2 Likes

Rogier i dont know how to thank you my friend !

Its almost done and i understood the technique . It will be sure very helpfull for my journey with SQL in the future.

I made some corrections in the code in order to run , because it was broken in some places probably due to the copy / paste and conflict with the HTML editor of the answer.

SET ANSI_NULLS, QUOTED_IDENTIFIER, ANSI_PADDING ON;
GO

DROP TABLE IF EXISTS #Invoices;

CREATE TABLE #Invoices
(
Project Varchar(10) NOT NULL
,DocumentCode Varchar(15) NOT NULL
,IssueDate Date NOT NULL
,Cost Decimal(8,2) NOT NULL
,ConcernFrom Date NULL
,ConcernTo Date NULL

);
INSERT INTO #Invoices
VALUES ('Prj-001', 'DOC-50001', '20230601', 500 , NULL , NULL),
('Prj-001', 'DOC-50002', '20230615', 1000 , '20230615' , '20230720'),
('Prj-001', 'DOC-50003', '20230625', 300 , NULL , NULL),
('Prj-002', 'DOC-50004', '20230710', 250 , NULL , NULL),
('Prj-002', 'DOC-50005', '20230731', 100 , NULL , NULL),
('Prj-002', 'DOC-50006', '20231015', 500 , '20231215' , '20240131')

;WITH Years AS
(
SELECT 2022 AS [Year]
UNION
SELECT 2023 AS [Year]
UNION
SELECT 2024 AS [Year]
), Months AS
(
SELECT 1 AS [Month]
UNION
SELECT 2 AS [Month]
UNION
SELECT 3 AS [Month]
UNION
SELECT 4 AS [Month]
UNION
SELECT 5 AS [Month]
UNION
SELECT 6 AS [Month]
UNION
SELECT 7 AS [Month]
UNION
SELECT 8 AS [Month]
UNION
SELECT 9 AS [Month]
UNION
SELECT 10 AS [Month]
UNION
SELECT 11 AS [Month]
UNION
SELECT 12 AS [Month]
), InvoicePeriod AS
(
SELECT
Years.[Year],
Months.[Month], Years.[Year] *100 + Months.[Month] AS [InvoicePeriod],
DATEFROMPARTS(Years.Year,Months.Month,1) AS BeginOfMonth,
EOMONTH(DATEFROMPARTS(Years.Year,Months.Month,1)) EndOfMonth
FROM Years
CROSS JOIN Months
), InvoiceTable AS
(
SELECT
Project,
Year,
Month,
Cost,
CASE WHEN ConcernFrom > BeginOfMonth THEN ConcernFrom ELSE BeginOfMonth END AS StartDate,
CASE WHEN ConcernTo < EndOfMonth THEN ConcernTo ELSE EndOfMonth END AS EndDate,
DATEDIFF(DAY,ConcernFrom,ConcernTo) AS TotalDays,
DATEDIFF(DAY,CASE WHEN ConcernFrom > BeginOfMonth THEN ConcernFrom ELSE BeginOfMonth END,CASE WHEN ConcernTo < EndOfMonth THEN ConcernTo ELSE EndOfMonth END) DaysThisPeriod
FROM #Invoices
INNER JOIN InvoicePeriod ON
InvoicePeriod.[InvoicePeriod] BETWEEN YEAR(ConcernFrom)*100 + Month(ConcernFrom) AND YEAR(ConcernTo)*100 + Month(ConcernTo)
WHERE ConcernFrom IS NOT NULL
)
SELECT
tbl.Project, tbl.[Year], tbl.[Month], SUM(tbl.Cost)
FROM
(
SELECT I.Project, I.[Year], I.[Month], SUM((I.DaysThisPeriod*1.0/I.TotalDays*1.0)*[Cost]) AS Cost
FROM InvoiceTable I
GROUP BY I.Project,I.[Year], I.[Month]
UNION
SELECT
I.Project, YEAR(I.IssueDate), MONTH(I.IssueDate), SUM(Cost) AS Cost
FROM #Invoices I
WHERE ConcernFrom IS NULL
GROUP BY I.Project, YEAR(I.IssueDate), MONTH(I.IssueDate)
) tbl
GROUP BY Project, [Year], [Month];

I will try to dissagragate each step in order to find the reason for these slighlty differences , correct it and find out what you meant when you mentioned "that its not perfect as the number of days is not the same as your example"

542.86 Should be 571,42
319,15 Shoud be 329,78

Project	Year	Month	(No column name)
Prj-001	2023	6	1228.57
Prj-001	2023	7	542.86
Prj-002	2023	7	350.00
Prj-002	2023	12	170.21
Prj-002	2024	1	319.15

OK , i found it

What it was needed to be changed was from

DATEDIFF(DAY,CASE WHEN ConcernFrom > BeginOfMonth THEN ConcernFrom ELSE BeginOfMonth END,
CASE WHEN ConcernTo < EndOfMonth THEN ConcernTo ELSE EndOfMonth END) DaysThisPeriod

to

DATEDIFF(DAY,CASE WHEN ConcernFrom > BeginOfMonth THEN ConcernFrom ELSE BeginOfMonth END,
CASE WHEN ConcernTo <= EndOfMonth THEN dateadd(day,1,ConcernTo) ELSE EndOfMonth END) DaysThisPeriod

The Final query is

SET ANSI_NULLS, QUOTED_IDENTIFIER, ANSI_PADDING ON;
GO

DROP TABLE IF EXISTS Invoices;

CREATE TABLE Invoices
(
Project Varchar(10) NOT NULL
,DocumentCode Varchar(15) NOT NULL
,IssueDate Date NOT NULL
,Cost Decimal(8,2) NOT NULL
,ConcernFrom Date NULL
,ConcernTo Date NULL

);
INSERT INTO Invoices
VALUES ('Prj-001', 'DOC-50001', '20230601', 500 , NULL , NULL),
('Prj-001', 'DOC-50002', '20230615', 1000 , '20230615' , '20230720'),
('Prj-001', 'DOC-50003', '20230625', 300 , NULL , NULL),
('Prj-002', 'DOC-50004', '20230710', 250 , NULL , NULL),
('Prj-002', 'DOC-50005', '20230731', 100 , NULL , NULL),
('Prj-002', 'DOC-50006', '20231015', 500 , '20231215' , '20240131')

;WITH Years AS
(
SELECT 2022 AS [Year]
UNION
SELECT 2023 AS [Year]
UNION
SELECT 2024 AS [Year]
), Months AS
(
SELECT 1 AS [Month]
UNION
SELECT 2 AS [Month]
UNION
SELECT 3 AS [Month]
UNION
SELECT 4 AS [Month]
UNION
SELECT 5 AS [Month]
UNION
SELECT 6 AS [Month]
UNION
SELECT 7 AS [Month]
UNION
SELECT 8 AS [Month]
UNION
SELECT 9 AS [Month]
UNION
SELECT 10 AS [Month]
UNION
SELECT 11 AS [Month]
UNION
SELECT 12 AS [Month]
), InvoicePeriod AS
(
SELECT
Years.[Year],
Months.[Month], Years.[Year] *100 + Months.[Month] AS [InvoicePeriod],
DATEFROMPARTS(Years.Year,Months.Month,1) AS BeginOfMonth,
EOMONTH(DATEFROMPARTS(Years.Year,Months.Month,1)) EndOfMonth
FROM Years
CROSS JOIN Months
), InvoiceTable AS
(
SELECT
Project,
Year,
Month,
Cost,
CASE WHEN ConcernFrom > BeginOfMonth THEN ConcernFrom ELSE BeginOfMonth END AS StartDate,
CASE WHEN ConcernTo < EndOfMonth THEN ConcernTo ELSE EndOfMonth END AS EndDate,
DATEDIFF(DAY,ConcernFrom,ConcernTo) AS TotalDays,
DATEDIFF(DAY,CASE WHEN ConcernFrom > BeginOfMonth THEN ConcernFrom ELSE BeginOfMonth END,
CASE WHEN ConcernTo <= EndOfMonth THEN dateadd(day,1,ConcernTo) ELSE EndOfMonth END) DaysThisPeriod
FROM Invoices
INNER JOIN InvoicePeriod ON
InvoicePeriod.[InvoicePeriod] BETWEEN YEAR(ConcernFrom)*100 + Month(ConcernFrom) AND YEAR(ConcernTo)*100 + Month(ConcernTo)
WHERE ConcernFrom IS NOT NULL
)


SELECT
tbl.Project, tbl.[Year], tbl.[Month], SUM(tbl.Cost)
FROM
(
SELECT I.Project, I.[Year], I.[Month], SUM((I.DaysThisPeriod  *1.0/I.TotalDays*1.0)*[Cost]) AS Cost
FROM InvoiceTable I
GROUP BY I.Project,I.[Year], I.[Month]
UNION
SELECT
I.Project, YEAR(I.IssueDate), MONTH(I.IssueDate), SUM(Cost) AS Cost
FROM Invoices I
WHERE ConcernFrom IS NULL
GROUP BY I.Project, YEAR(I.IssueDate), MONTH(I.IssueDate)
) tbl
GROUP BY Project, [Year], [Month];

Result

Project	Year	Month	(No column name)
Prj-001	2023	6	1228.57
Prj-001	2023	7	571.43
Prj-002	2023	7	350.00
Prj-002	2023	12	170.21
Prj-002	2024	1	329.79

Correct Results in this case
but when i try changing some input data it gives some non correct values ..
I'm in making a change that solves some combinations but with other combinations
it's stop working correctly and vice versa.

Any extra help would be appreciated ... The problem is in DaysThisPeriod Field that doesnt work correctly for every case. :frowning:

I think you need to step down from the large query and just build a simple example with the cases you have. Then try to explain and after that try to solve it and when you have solved it compare it with the large query. If you can not explain it, it will be hard to implement in any query, small or big.

If you cannot solve it you have a good starting point of asking it on this forum.