2016-03-03 3 views
1

Мне удалось найти скрипт, который задавал диапазон дат начала и окончания, он будет создавать новые строки в зависимости от диапазона дат. Проблема, с которой я сталкиваюсь, заключается в том, что для каждой записи у меня есть поле AMOUNT. Мне нужно правильно проректировать диапазон дат.Возвращаемое пропорциональное количество из диапазона дат

CREATE TABLE #TempData (Company VARCHAR(6), InvoiceDate DATE, StartPeriod DATE, EndPeriod DATE, SchoolDistrict VARCHAR(100), Amount NUMERIC(10,2)) 
INSERT INTO #TempData (Company,InvoiceDate,StartPeriod,EndPeriod,SchoolDistrict,Amount) 

SELECT '000123','1/1/2016','12/1/2015','12/31/2015','School District 123',140 UNION ALL 
SELECT '000123','12/1/2016','6/15/2015','11/30/2015','School District 123',500 

;WITH Recurse AS (
SELECT Company,InvoiceDate, StartPeriod 
,CAST(DATEADD(DAY,-1,DATEADD(MONTH,DATEDIFF(MONTH,0,StartPeriod)+1,0)) AS DATE) EOM,EndPeriod 
,SchoolDistrict,Amount 
FROM #TempData 

UNION ALL 

SELECT Company,InvoiceDate 
,CAST(DATEADD(MONTH,DATEDIFF(MONTH,0,StartPeriod)+1,0) AS DATE) StartPeriod 
,CAST(DATEADD(DAY,-1,DATEADD(MONTH,DATEDIFF(MONTH,0,StartPeriod)+2,0)) AS DATE) 
,EndPeriod 
,SchoolDistrict,Amount 
FROM Recurse 
WHERE EOM<EndPeriod 
) 

SELECT Company,InvoiceDate,StartPeriod 
,CASE WHEN EndPeriod<EOM THEN EndPeriod ELSE EOM END EndPeriod 
,SchoolDistrict,Amount 
FROM Recurse 

DROP TABLE TempData 

Моего выход выглядит следующим образом:

Company InvoiceDate StartPeriod EndPeriod SchoolDistrict  Amount 
00-01-01 2015-12-01 2015-12-31 School District 123 140.00 
00-12-01 2015-06-15 2015-06-30 School District 123 500.00 
00-12-01 2015-07-01 2015-07-31 School District 123 500.00 
00-12-01 2015-08-01 2015-08-31 School District 123 500.00 
00-12-01 2015-09-01 2015-09-30 School District 123 500.00 
00-12-01 2015-10-01 2015-10-31 School District 123 500.00 
00-12-01 2015-11-01 2015-11-30 School District 123 500.00 

Что касается первого возвращения рекорда, нет необходимости делать какое-либо пропорциональное распределение, как это только на 1 месяц, но другие записи, я не буду, нуждающейся в помощи о том, как правильно правильно распределить AMOUNT из 500 правильно по 6 возвращенным.

ПРИМЕЧАНИЕ Обновление: В течение полных месяцев равное распределение, то любые месяцы StartPeriod и EndPeriod, которые не являются полными периодами, получают частичное распределение пропорциональности.

+0

Вы пробовали группу? – Yossi

+0

Можете ли вы просто использовать 'DATEDIFF()', чтобы получить количество месяцев, необходимое для деления суммы на? –

+0

Вам нужно будет описать, как вы хотите сделать распространение. Должны ли все полные месяцы получать равные суммы с остатком, указанным в частичные месяцы в начале? Может ли быть и неполный месяц в конце? Или вы просто хотите выделить по количеству дней в месяце? – shawnt00

ответ

2

Здесь представлена ​​цепочка выражений, полученная из исходных дат ввода и количества. Вы можете легко подать это в свой метод Recurse, хотя я рекомендую один из других методов генерации месяцев, например, используя таблицу чисел, особенно если даты могут варьироваться в течение многих лет.

Для частичных месяцев он вычисляет долю в зависимости от количества дней, охватываемых в этом месяце. Делитель - это общее количество дней в этом месяце. Иногда бухгалтеры рассматривают месяц как имеющий 30 дней, поэтому вам придется решить, подходит ли это.

Полная сумма делится на полные месяцы, взвешенная одинаково независимо от длины, плюс две частичные суммы, взвешенные по их индивидуальным пропорциям их соответствующих месяцев. Сначала вычисляется полная сумма месяца, и результат округляется; частичные месяцы зависят от этого расчета и отмечают мой комментарий в конце о последствиях округления к копейке. Окончательные результаты должны быть осторожны, чтобы правильно распределить последнюю пенни, чтобы сумма была правильной.

with Expr1 as (
select *, 
    StartPeriod as RangeStart, EndPeriod as RangeEnd, 

    case when datediff(month, StartPeriod, EndPeriod) < 1 then null else 
     datediff(month, StartPeriod, EndPeriod) + 1 
      - case when datepart(day, StartPeriod) <> 1 
        then 1 else 0 end 
      - case when month(EndPeriod) = month(dateadd(day, 1, EndPeriod)) 
        then 1 else 0 end 
    end as WholeMonths, 

    case when datepart(day, StartPeriod) <> 1 
     then 1 else 0 end as IsPartialStart, 
    case when month(EndPeriod) = month(dateadd(day, 1, EndPeriod)) 
     then 1 else 0 end as IsPartialEnd, 

    datepart(day, StartPeriod) as StartPartialComplement, 
    datepart(day, EndPeriod) as EndPartialOffset, 

    datepart(day, 
     dateadd(day, -1, dateadd(month, datediff(month, 0, StartPeriod) + 1, 0)) 
    ) as StartPartialDaysInMonth, 

    datepart(day, 
     dateadd(day, -1, dateadd(month, datediff(month, 0, EndPeriod) + 1, 0)) 
    ) as EndPartialDaysInMonth 
from #TempData 
), 

Expr2 as (
select *, 
    case when IsPartialStart = 1 
     then StartPartialDaysInMonth - StartPartialComplement + 1 
     else 0 end as StartPartialDays, 
    case when IsPartialEnd = 1 
     then EndPartialOffset else 0 end as EndPartialDays 
from Expr1 
), 

Expr3 as (
select *, 
    cast(round(Amount/(
     WholeMonths 
      + StartPartialDays/cast(StartPartialDaysInMonth as float) 
      + EndPartialDays/cast(EndPartialDaysInMonth as float) 
    ), 2) as numeric(10, 2)) as WholeMonthAllocation, 
    StartPartialDays/cast(StartPartialDaysInMonth as float) as StartPartialFraction, 
    EndPartialDays/cast(EndPartialDaysInMonth as float) as EndPartialFraction 
from Expr2 
), 

Expr4 as (
select *, 
    cast(case when IsPartialEnd = 0 
     then Amount - WholeMonthAllocation * WholeMonths 
     else StartPartialFraction * WholeMonthAllocation 
     end as numeric(10, 2)) as StartPartialAmount, 
    cast(case when IsPartialEnd = 0 then 0 
       else Amount 
        - WholeMonthAllocation * WholeMonths 
        - StartPartialFraction * WholeMonthAllocation 
     end as numeric(10, 2)) as EndPartialAmount 
from Expr3 
), 
... 

Из этих значений вы можете определить, какая сумма должна закончиться в конечном результате после того, как вы создали все дополнительные строки. Это выражение сделает трюк, включив ваш исходный запрос. (Поскольку SQL Скрипки был вниз, я не был в состоянии проверить все это :)

... /* all of the above */ 
Recurse AS (
SELECT 
    RangeStart, RangeEnd, IsPartialStart, IsPartialEnd, 
    StartPartialAmount, EndPartialAmount, WholeMonthAllocation, 
    Company, InvoiceDate, StartPeriod, 
    CAST(DATEADD(DAY,-1,DATEADD(MONTH,DATEDIFF(MONTH,0,StartPeriod)+1,0)) AS DATE) EOM, 
    EndPeriod, SchoolDistrict, 
    case 
     when datediff(month, RangeStart, RangeEnd) = 0 then Amount 
     when IsPartialStart = 1 then StartPartialAmount 
     else WholeMonthAllocation 
    end as Amount 
FROM Expr4 
UNION ALL 
SELECT 
    RangeStart, RangeEnd, IsPartialStart, IsPartialEnd, 
    StartPartialAmount, EndPartialAmount, WholeMonthAllocation, 
    Company, InvoiceDate, 
    CAST(DATEADD(MONTH,DATEDIFF(MONTH,0,StartPeriod)+1,0) AS DATE) AS StartPeriod, 
    CAST(DATEADD(DAY,-1,DATEADD(MONTH,DATEDIFF(MONTH,0,StartPeriod)+2,0)) AS DATE) EOM, 
    EndPeriod, SchoolDistrict, 
    case 
     -- final month is when StartPeriod is one month before RangeEnd. 
     -- remember this is recursive! 
     when IsPartialEnd = 1 and datediff(month, StartPeriod, RangeEnd) = 1 
     then EndPartialAmount 
     else WholeMonthAllocation 
    end as Amount 
FROM Recurse 
WHERE EOM < EndPeriod 
) 
SELECT 
    Company, InvoiceDate, StartPeriod, 
    CASE WHEN EndPeriod < EOM THEN EndPeriod ELSE EOM END EndPeriod, 
    SchoolDistrict, Amount 
FROM Recurse 

Я добавил/псевдонимами RangeStart и RangeEnd значения, чтобы избежать путаницы с StartPeriod и EndPeriod, которые вы используете в как вашу временную таблицу, так и выходной запрос. Значения Range- представляют собой начало и конец полного диапазона, а значения Period - - это вычисленные значения, которые вызывают отдельные периоды. Адаптируйте, как вы считаете нужным.

Редактировать # 1: Я понял, что не обработал случай, когда начало и конец падают в том же месяце: возможно, есть более чистый способ сделать все это. Я просто закончил обнуление выражения WholeMonths, чтобы избежать возможного деления на ноль. Выражение case в конце выхватывает это условие и просто возвращает исходное значение Amount. Хотя вам, вероятно, не нужно беспокоиться о том, чтобы разобраться с датой начала и окончания, меня перевернули, я пошел вперед и привязал их все вместе с тем же самым тестом < 1.

Редактировать # 2: Как только у меня было место, чтобы попробовать это, ваш тестовый пример показал, что округление теряло копейки и подбирало окончательный расчет частичного месяца, даже когда это было фактически одно из целого месяцы. Поэтому мне пришлось приспособиться к тому, чтобы искать случай, когда нет окончательного частичного месяца. Это находится в Expr4. Я также заметил несколько незначительных синтаксических ошибок, которые вы отметили.

Рекурсивный запрос позволяет видеть месяцы в порядке и немного упрощает логику. Якорь всегда будет начальным месяцем, поэтому ни одна из последних месяцев логики не применяется и аналогично для другой половины запроса. Если вы в конечном итоге переключение это с регулярным присоединиться к таблице чисел вы хотите использовать выражение, как это вместо:

case 
    when datediff(month, RangeStart, RangeEnd) = 0 
    then Amount 
    when IsPartialStart = 1 and is first month... 
    then StartPartialAmount 
    when IsPartialEnd = 1 and is final month... 
    then EndPartialAmount 
    else WholeMonthAllocation 
end as Amount 

Edit # 3: Также следует помнить, что этот метод не подходит, когда имея дело с очень небольшими суммами, где округление будет искажать результаты. Примеры:

$ 0.13, разделенный с 02 по 01 декабря, дает [.01, .01, .01, .01, .01, .01, .01, .01, .01, .01, .01, .02 ] $ 0.08, разделённый с 02 января по 01 декабря, дает [.01, .01, .01, .01, .01, .01, .01, .01, .01, .01, .01, -.03] $ 0.08 с 31 января по 31 декабря, дает [-.03, .01, .01, .01, .01, .01, .01, .01, .01, .01, .01, .01] $ 0,05, разделенный 31 января до 30 ноября дает [.05, .00, .00, .00, .00, .00, .00, .00, .00, .00, .00] $ 0,05, разделенный 31 января на 01 декабря, дает [.00 , .00, .00, .00, .00, .00, .00, .00, .00, .00, .00, .05] $ 0,30, разделенный с 02 января по 1 марта, дает [.15, .15, .00]

+0

Благодарим вас за предоставление этого. Это поможет мне понять, что у вас есть. Но когда вы говорите «Вы можете подать это в свой метод Recurse, если не будет ограничений на рекурсивные CTE, о которых я не знаю. «Я не уверен, как ваш код вставляется в мой код. –

+0

Я попытался добавить вашу дополнительную информацию, но у меня все еще возникают проблемы. Сначала возникли некоторые проблемы с ошибками, но я смог понять это. Часть, с которой я сталкиваюсь, заключается в том, что когда вы добавляете репитер CTE из моего исходного сообщения, UNION ALL вызывает некоторые проблемы, потому что столбцы не равны, поэтому я не могу правильно понять, как это будет работать. Спасибо за то, что вы предоставили до сих пор. –

+0

@Chuck Я на самом деле работаю над этим прямо сейчас и имею рабочую версию. – shawnt00

1

Это интересная проблема, потому что она требует как расширения числа строк, так и из-за проблем округления, которые могут быть обнаружены и исправлены в запросе.

Прежде всего, необходимо определить, сколько дней в каждом месяце попадает в период StartPeriod и EndPeriod.

Затем оценка рассчитывается для каждого месяца в виде простой пропорции, но ошибки округления будут означать, что сумма этих оценок не суммируется с общей суммой счета-фактуры. Функции окна затем используются для вычисления общей ошибки округления, так что последний платеж может быть скорректирован.

В качестве альтернативы вместо генерации строки для каждого месяца с использованием рекурсивного CTE рекомендуется использовать соединение с простым представлением «числа». Для получения дополнительной информации см это question about number tables

-- I use the #tempdata table mentioned in the question 

; WITH numbers AS (-- A fast way to get a sequence of integers starting at 0 
    SELECT TOP(10000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 as n 
    FROM sys.all_columns a CROSS JOIN sys.all_columns b 
), 
data_with_pk AS (-- Add a primary key so that we know how to sort output 
    SELECT ROW_NUMBER() OVER (ORDER BY company, invoicedate) AS InvoiceId, * 
    FROM #tempdata 
), 
step1 AS (-- Calc first and last day of each month in which payment is due 
    SELECT data_with_pk.*, 
     CAST(DATEADD(MONTH, DATEDIFF(MONTH, 0, StartPeriod) + numbers.n, 0) 
      AS DATE) AS StartOfMonth, 
     CAST(DATEADD(DAY, -1, 
        DATEADD(MONTH, DATEDIFF(MONTH,0,StartPeriod) + numbers.n + 1, 0)) 
      AS DATE) AS EndOfMonth 
    FROM data_with_pk 
    -- This join is a simpler way to generate multiple rows than using a recursive CTE 
    JOIN numbers ON numbers.n <= DATEDIFF(MONTH, StartPeriod, EndPeriod) 
), 
step2 AS (-- Calc block of days in each month which fall within whole period 
    SELECT *, 
     CASE WHEN StartPeriod > StartOfMonth THEN StartPeriod ELSE StartOfMonth END 
      AS StartOfBlock, 
     CASE WHEN EndPeriod < EndOfMonth THEN EndPeriod ELSE EndOfMonth END 
      AS EndOfBlock 
    FROM step1 
), 
step3 AS (-- Whole months count as 30 days for purposes of calculated proportions 
    SELECT *, 
     CASE WHEN StartOfBlock = StartOfMonth AND EndOfBlock = EndOfMonth 
      THEN 30 
      ELSE DATEDIFF(DAY, StartOfBlock, EndOfBlock) + 1 END AS DaysInBlock 
    FROM step2 
), 
step3b AS (
    SELECT *, 
     SUM(DaysInBlock) OVER (PARTITION BY InvoiceId) AS DaysInPeriod 
    FROM step3 
), 
step4 AS (-- Calc proportion of whole amount due in this block 
    SELECT *, 
     CAST(Amount * DaysInBlock/DaysInPeriod AS NUMERIC(10,2)) AS Estimate 
    FROM step3b 
), 
step5 AS (-- Calc running total of estimates 
    SELECT *, 
     SUM(Estimate) OVER (PARTITION BY InvoiceId ORDER BY EndOfBlock) AS RunningEstimate 
    FROM step4 
), 
step6 AS (-- Adjust last estimate to ensure final Prorata total is equal to Amount 
    SELECT *, 
     CASE WHEN EndOfBlock = EndPeriod 
      THEN Estimate + amount - RunningEstimate 
      ELSE Estimate end AS Prorata 
    FROM step5 
), 
step7 AS (-- Just for illustration to prove that payments sum to the Invoice Amount 
    SELECT *, 
     SUM(Prorata) OVER (PARTITION BY InvoiceId ORDER BY EndOfBlock) AS RunningProrata 
    FROM step6 
) 
SELECT InvoiceId, InvoiceDate, StartPeriod, EndPeriod, Amount, DaysInBlock, EndOfBlock, 
    Estimate, RunningEstimate, Prorata, RunningProrata 
FROM step7 
ORDER BY InvoiceId, EndOfBlock 

Вы можете увидеть «Оценка» и столбцы «RunningEstimate» в результате устанавливается ниже в конечном итоге $ 0,01, но исправляются в колонке «Prorata».

+-----------+-------------+-------------+------------+--------+-------------+------------+----------+-----------------+---------+----------------+ 
| InvoiceId | InvoiceDate | StartPeriod | EndPeriod | Amount | DaysInBlock | EndOfBlock | Estimate | RunningEstimate | Prorata | RunningProrata | 
+-----------+-------------+-------------+------------+--------+-------------+------------+----------+-----------------+---------+----------------+ 
|   1 | 2016-01-01 | 2015-12-01 | 2015-12-31 | 140.00 |   30 | 2015-12-31 | 140.00 | 140.00   | 140.00 | 140.00   | 
|   2 | 2016-12-01 | 2015-06-15 | 2015-11-30 | 500.00 |   16 | 2015-06-30 | 48.19 | 48.19   | 48.19 | 48.19   | 
|   2 | 2016-12-01 | 2015-06-15 | 2015-11-30 | 500.00 |   30 | 2015-07-31 | 90.36 | 138.55   | 90.36 | 138.55   | 
|   2 | 2016-12-01 | 2015-06-15 | 2015-11-30 | 500.00 |   30 | 2015-08-31 | 90.36 | 228.91   | 90.36 | 228.91   | 
|   2 | 2016-12-01 | 2015-06-15 | 2015-11-30 | 500.00 |   30 | 2015-09-30 | 90.36 | 319.27   | 90.36 | 319.27   | 
|   2 | 2016-12-01 | 2015-06-15 | 2015-11-30 | 500.00 |   30 | 2015-10-31 | 90.36 | 409.63   | 90.36 | 409.63   | 
|   2 | 2016-12-01 | 2015-06-15 | 2015-11-30 | 500.00 |   30 | 2015-11-30 | 90.36 | 499.99   | 90.37 | 500.00   | 
+-----------+-------------+-------------+------------+--------+-------------+------------+----------+-----------------+---------+----------------+ 
+0

Просто увидел ваш комментарий, что полные месяцы должны получать равное распределение, но моя логика использует фактические дни в каждом месяце. Я постараюсь приспособиться, если у меня будет время. –

+0

Благодарим вас за представление. Мне было бы интересно увидеть, как это работает с равной логикой распределения в течение полных месяцев, когда у вас есть время, чтобы изменить его. –

+0

было бы также распределение прората на частичные месяцы в случае, когда начальный период составляет 6/15/15, а конечный период - 11/18/15. Я буду ждать ваших изменений, когда у вас будет время. –

Смежные вопросы