ENH scale taxes by pay period length
This commit is contained in:
parent
397a78ddfb
commit
5e2e8d8acf
|
@ -787,6 +787,52 @@ let MultiAllocation =
|
||||||
-}
|
-}
|
||||||
Allocation Interval
|
Allocation Interval
|
||||||
|
|
||||||
|
let HourlyPeriod =
|
||||||
|
{-
|
||||||
|
Definition for a pay period denominated in hours
|
||||||
|
-}
|
||||||
|
{ hpAnnualHours :
|
||||||
|
{-
|
||||||
|
Number of hours in one year
|
||||||
|
-}
|
||||||
|
Natural
|
||||||
|
, hpDailyHours :
|
||||||
|
{-
|
||||||
|
Number of hours in one working day
|
||||||
|
-}
|
||||||
|
Natural
|
||||||
|
, hpWorkingDays :
|
||||||
|
{-
|
||||||
|
Days which count as working days
|
||||||
|
-}
|
||||||
|
List Weekday
|
||||||
|
}
|
||||||
|
|
||||||
|
let PeriodType =
|
||||||
|
{-
|
||||||
|
Type of pay period.
|
||||||
|
|
||||||
|
Hourly: pay period is denominated in hours
|
||||||
|
Daily: pay period is denominated in working days (specified in a list)
|
||||||
|
-}
|
||||||
|
< Hourly : HourlyPeriod | Daily : List Weekday >
|
||||||
|
|
||||||
|
let Period =
|
||||||
|
{-
|
||||||
|
Definition of a pay period
|
||||||
|
-} { pType :
|
||||||
|
{-
|
||||||
|
Type of pay period
|
||||||
|
-}
|
||||||
|
PeriodType
|
||||||
|
, pStart :
|
||||||
|
{-
|
||||||
|
Start date of the pay period. Must occur before first payment
|
||||||
|
in this income stream is dispersed.
|
||||||
|
-}
|
||||||
|
Gregorian
|
||||||
|
}
|
||||||
|
|
||||||
let Income =
|
let Income =
|
||||||
{-
|
{-
|
||||||
Means to compute an income stream and how to allocate it
|
Means to compute an income stream and how to allocate it
|
||||||
|
@ -807,6 +853,12 @@ let Income =
|
||||||
The dates on which the income stream is distributed.
|
The dates on which the income stream is distributed.
|
||||||
-}
|
-}
|
||||||
DatePat
|
DatePat
|
||||||
|
, incPayPeriod :
|
||||||
|
{-
|
||||||
|
Defines the period of time over which this income was earned
|
||||||
|
(mostly used for taxes)
|
||||||
|
-}
|
||||||
|
Period
|
||||||
, incPretax : List (SingleAllocation PretaxValue)
|
, incPretax : List (SingleAllocation PretaxValue)
|
||||||
, incTaxes : List (SingleAllocation TaxValue)
|
, incTaxes : List (SingleAllocation TaxValue)
|
||||||
, incPosttax : List (SingleAllocation PosttaxValue)
|
, incPosttax : List (SingleAllocation PosttaxValue)
|
||||||
|
@ -1029,4 +1081,7 @@ in { CurID
|
||||||
, HistTransfer
|
, HistTransfer
|
||||||
, SingleAllocation
|
, SingleAllocation
|
||||||
, MultiAllocation
|
, MultiAllocation
|
||||||
|
, HourlyPeriod
|
||||||
|
, Period
|
||||||
|
, PeriodType
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,6 +105,19 @@ withDates dp f = do
|
||||||
days <- liftExcept $ expandDatePat bounds dp
|
days <- liftExcept $ expandDatePat bounds dp
|
||||||
combineErrors $ fmap f days
|
combineErrors $ fmap f days
|
||||||
|
|
||||||
|
foldDates
|
||||||
|
:: (MonadSqlQuery m, MonadFinance m, MonadInsertError m)
|
||||||
|
=> DatePat
|
||||||
|
-> Day
|
||||||
|
-> (Day -> Day -> m a)
|
||||||
|
-> m [a]
|
||||||
|
foldDates dp start f = do
|
||||||
|
bounds <- askDBState kmBudgetInterval
|
||||||
|
days <- liftExcept $ expandDatePat bounds dp
|
||||||
|
combineErrors $
|
||||||
|
snd $
|
||||||
|
L.mapAccumL (\prevDay day -> (day, f prevDay day)) start days
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- budget
|
-- budget
|
||||||
|
|
||||||
|
@ -279,29 +292,42 @@ insertIncome
|
||||||
key
|
key
|
||||||
name
|
name
|
||||||
(intPre, intTax, intPost)
|
(intPre, intTax, intPost)
|
||||||
Income {incWhen, incCurrency, incFrom, incPretax, incPosttax, incTaxes, incToBal, incGross} = do
|
Income
|
||||||
|
{ incWhen
|
||||||
|
, incCurrency
|
||||||
|
, incFrom
|
||||||
|
, incPretax
|
||||||
|
, incPosttax
|
||||||
|
, incTaxes
|
||||||
|
, incToBal
|
||||||
|
, incGross
|
||||||
|
, incPayPeriod
|
||||||
|
} = do
|
||||||
-- TODO check that the other accounts are not income somewhere here
|
-- TODO check that the other accounts are not income somewhere here
|
||||||
_ <- checkAcntType IncomeT $ taAcnt incFrom
|
_ <- checkAcntType IncomeT $ taAcnt incFrom
|
||||||
precision <- lookupCurrencyPrec incCurrency
|
precision <- lookupCurrencyPrec incCurrency
|
||||||
|
let gross = roundPrecision precision incGross
|
||||||
-- TODO this will scan the interval allocations fully each time
|
-- TODO this will scan the interval allocations fully each time
|
||||||
-- iteration which is a total waste, but the fix requires turning this
|
-- iteration which is a total waste, but the fix requires turning this
|
||||||
-- loop into a fold which I don't feel like doing now :(
|
-- loop into a fold which I don't feel like doing now :(
|
||||||
let gross = roundPrecision precision incGross
|
res <- foldDates incWhen start (allocate precision gross)
|
||||||
res <- withDates incWhen (allocate precision gross)
|
|
||||||
return $ concat res
|
return $ concat res
|
||||||
where
|
where
|
||||||
|
start = fromGregorian' $ pStart incPayPeriod
|
||||||
|
pType' = pType incPayPeriod
|
||||||
meta = BudgetMeta key name
|
meta = BudgetMeta key name
|
||||||
flatPre = concatMap flattenAllo incPretax
|
flatPre = concatMap flattenAllo incPretax
|
||||||
flatTax = concatMap flattenAllo incTaxes
|
flatTax = concatMap flattenAllo incTaxes
|
||||||
flatPost = concatMap flattenAllo incPosttax
|
flatPost = concatMap flattenAllo incPosttax
|
||||||
sumAllos = sum . fmap faValue
|
sumAllos = sum . fmap faValue
|
||||||
-- TODO ensure these are all the "correct" accounts
|
-- TODO ensure these are all the "correct" accounts
|
||||||
allocate precision gross day =
|
allocate precision gross prevDay day = do
|
||||||
|
scaler <- liftExcept $ periodScaler pType' prevDay day
|
||||||
let (preDeductions, pre) =
|
let (preDeductions, pre) =
|
||||||
allocatePre precision gross $
|
allocatePre precision gross $
|
||||||
flatPre ++ concatMap (selectAllos day) intPre
|
flatPre ++ concatMap (selectAllos day) intPre
|
||||||
tax =
|
tax =
|
||||||
allocateTax precision gross preDeductions $
|
allocateTax precision gross preDeductions scaler $
|
||||||
flatTax ++ concatMap (selectAllos day) intTax
|
flatTax ++ concatMap (selectAllos day) intTax
|
||||||
aftertaxGross = sumAllos $ tax ++ pre
|
aftertaxGross = sumAllos $ tax ++ pre
|
||||||
post =
|
post =
|
||||||
|
@ -322,6 +348,41 @@ insertIncome
|
||||||
then throwError $ InsertException [IncomeError day name balance]
|
then throwError $ InsertException [IncomeError day name balance]
|
||||||
else return (bal : fmap (allo2Trans meta day incFrom) (pre ++ tax ++ post))
|
else return (bal : fmap (allo2Trans meta day incFrom) (pre ++ tax ++ post))
|
||||||
|
|
||||||
|
type PeriodScaler = Natural -> Double -> Double
|
||||||
|
|
||||||
|
-- TODO we probably don't need to check for 1/0 each time
|
||||||
|
periodScaler
|
||||||
|
:: PeriodType
|
||||||
|
-> Day
|
||||||
|
-> Day
|
||||||
|
-> InsertExcept PeriodScaler
|
||||||
|
periodScaler pt prev cur
|
||||||
|
| interval > 0 = return scale
|
||||||
|
-- TODO fix error here
|
||||||
|
| otherwise = throwError $ InsertException undefined
|
||||||
|
where
|
||||||
|
interval = diffDays cur prev
|
||||||
|
startDay = dayOfWeek prev
|
||||||
|
days = L.sort $
|
||||||
|
fmap (diff startDay . fromWeekday) $
|
||||||
|
L.nub $ case pt of
|
||||||
|
Hourly HourlyPeriod {hpWorkingDays} -> hpWorkingDays
|
||||||
|
Daily ds -> ds
|
||||||
|
diff a b = fromIntegral $ mod (fromEnum a - fromEnum b) 7
|
||||||
|
n =
|
||||||
|
let (nFull, nPart) = divMod interval 7
|
||||||
|
daysFull = fromIntegral (length days) * nFull
|
||||||
|
daysTail = fromIntegral $ length $ takeWhile (< nPart) days
|
||||||
|
in fromIntegral $ daysFull + daysTail
|
||||||
|
scale precision x = case pt of
|
||||||
|
Hourly HourlyPeriod {hpAnnualHours, hpDailyHours} ->
|
||||||
|
fromRational (rnd $ x / fromIntegral hpAnnualHours)
|
||||||
|
* fromIntegral hpDailyHours
|
||||||
|
* n
|
||||||
|
Daily _ -> x * n / 365.25
|
||||||
|
where
|
||||||
|
rnd = roundPrecision precision
|
||||||
|
|
||||||
allocatePre
|
allocatePre
|
||||||
:: Natural
|
:: Natural
|
||||||
-> Rational
|
-> Rational
|
||||||
|
@ -359,16 +420,18 @@ allocateTax
|
||||||
:: Natural
|
:: Natural
|
||||||
-> Rational
|
-> Rational
|
||||||
-> M.Map T.Text Rational
|
-> M.Map T.Text Rational
|
||||||
|
-> PeriodScaler
|
||||||
-> [FlatAllocation TaxValue]
|
-> [FlatAllocation TaxValue]
|
||||||
-> [FlatAllocation Rational]
|
-> [FlatAllocation Rational]
|
||||||
allocateTax precision gross deds = fmap (fmap go)
|
allocateTax precision gross preDeds f = fmap (fmap go)
|
||||||
where
|
where
|
||||||
go TaxValue {tvCategories, tvMethod} =
|
go TaxValue {tvCategories, tvMethod} =
|
||||||
let agi = gross - sum (mapMaybe (`M.lookup` deds) tvCategories)
|
let agi = gross - sum (mapMaybe (`M.lookup` preDeds) tvCategories)
|
||||||
in case tvMethod of
|
in case tvMethod of
|
||||||
TMPercent p -> roundPrecision 3 p / 100 * agi
|
TMPercent p -> roundPrecision 3 p / 100 * agi
|
||||||
TMBracket TaxProgression {tpDeductible, tpBrackets} ->
|
TMBracket TaxProgression {tpDeductible, tpBrackets} ->
|
||||||
foldBracket precision (agi - roundPrecision precision tpDeductible) tpBrackets
|
let taxDed = roundPrecision precision $ f precision tpDeductible
|
||||||
|
in foldBracket f precision (agi - taxDed) tpBrackets
|
||||||
|
|
||||||
allocatePost
|
allocatePost
|
||||||
:: Natural
|
:: Natural
|
||||||
|
@ -383,11 +446,11 @@ allocatePost precision aftertax = fmap (fmap go)
|
||||||
then aftertax * roundPrecision 3 v / 100
|
then aftertax * roundPrecision 3 v / 100
|
||||||
else roundPrecision precision v
|
else roundPrecision precision v
|
||||||
|
|
||||||
foldBracket :: Natural -> Rational -> [TaxBracket] -> Rational
|
foldBracket :: PeriodScaler -> Natural -> Rational -> [TaxBracket] -> Rational
|
||||||
foldBracket precision agi bs = fst $ foldr go (0, agi) $ L.sortOn tbLowerLimit bs
|
foldBracket f precision agi bs = fst $ foldr go (0, agi) $ L.sortOn tbLowerLimit bs
|
||||||
where
|
where
|
||||||
go TaxBracket {tbLowerLimit, tbPercent} (acc, remain) =
|
go TaxBracket {tbLowerLimit, tbPercent} (acc, remain) =
|
||||||
let l = roundPrecision precision tbLowerLimit
|
let l = roundPrecision precision $ f precision tbLowerLimit
|
||||||
p = roundPrecision 3 tbPercent / 100
|
p = roundPrecision 3 tbPercent / 100
|
||||||
in if remain < l then (acc + p * (remain - l), l) else (acc, remain)
|
in if remain < l then (acc + p * (remain - l), l) else (acc, remain)
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ makeHaskellTypesWith
|
||||||
, MultipleConstructors "BudgetCurrency" "(./dhall/Types.dhall).BudgetCurrency"
|
, MultipleConstructors "BudgetCurrency" "(./dhall/Types.dhall).BudgetCurrency"
|
||||||
, MultipleConstructors "BudgetTransferType" "(./dhall/Types.dhall).BudgetTransferType"
|
, MultipleConstructors "BudgetTransferType" "(./dhall/Types.dhall).BudgetTransferType"
|
||||||
, MultipleConstructors "TaxMethod" "(./dhall/Types.dhall).TaxMethod"
|
, MultipleConstructors "TaxMethod" "(./dhall/Types.dhall).TaxMethod"
|
||||||
|
, MultipleConstructors "PeriodType" "(./dhall/Types.dhall).PeriodType"
|
||||||
, SingleConstructor "Currency" "Currency" "(./dhall/Types.dhall).Currency"
|
, SingleConstructor "Currency" "Currency" "(./dhall/Types.dhall).Currency"
|
||||||
, SingleConstructor "Tag" "Tag" "(./dhall/Types.dhall).Tag"
|
, SingleConstructor "Tag" "Tag" "(./dhall/Types.dhall).Tag"
|
||||||
, SingleConstructor "TaggedAcnt" "TaggedAcnt" "(./dhall/Types.dhall).TaggedAcnt"
|
, SingleConstructor "TaggedAcnt" "TaggedAcnt" "(./dhall/Types.dhall).TaggedAcnt"
|
||||||
|
@ -66,6 +67,8 @@ makeHaskellTypesWith
|
||||||
, SingleConstructor "TaxValue" "TaxValue" "(./dhall/Types.dhall).TaxValue"
|
, SingleConstructor "TaxValue" "TaxValue" "(./dhall/Types.dhall).TaxValue"
|
||||||
, SingleConstructor "PosttaxValue" "PosttaxValue" "(./dhall/Types.dhall).PosttaxValue"
|
, SingleConstructor "PosttaxValue" "PosttaxValue" "(./dhall/Types.dhall).PosttaxValue"
|
||||||
, SingleConstructor "BudgetTransferValue" "BudgetTransferValue" "(./dhall/Types.dhall).BudgetTransferValue"
|
, SingleConstructor "BudgetTransferValue" "BudgetTransferValue" "(./dhall/Types.dhall).BudgetTransferValue"
|
||||||
|
, SingleConstructor "Period" "Period" "(./dhall/Types.dhall).Period"
|
||||||
|
, SingleConstructor "HourlyPeriod" "HourlyPeriod" "(./dhall/Types.dhall).HourlyPeriod"
|
||||||
-- , SingleConstructor "ToTx" "ToTx" "(./dhall/Types.dhall).ToTx"
|
-- , SingleConstructor "ToTx" "ToTx" "(./dhall/Types.dhall).ToTx"
|
||||||
-- , SingleConstructor "FieldMatcher" "FieldMatcher" "(./dhall/Types.dhall).FieldMatcher_"
|
-- , SingleConstructor "FieldMatcher" "FieldMatcher" "(./dhall/Types.dhall).FieldMatcher_"
|
||||||
-- , SingleConstructor "Match" "Match" "(./dhall/Types.dhall).Match_"
|
-- , SingleConstructor "Match" "Match" "(./dhall/Types.dhall).Match_"
|
||||||
|
@ -110,6 +113,9 @@ deriveProduct
|
||||||
, "PosttaxValue"
|
, "PosttaxValue"
|
||||||
, "BudgetTransferValue"
|
, "BudgetTransferValue"
|
||||||
, "BudgetTransferType"
|
, "BudgetTransferType"
|
||||||
|
, "Period"
|
||||||
|
, "PeriodType"
|
||||||
|
, "HourlyPeriod"
|
||||||
]
|
]
|
||||||
|
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
|
@ -229,8 +235,15 @@ data Income = Income
|
||||||
, incPosttax :: [SingleAllocation PosttaxValue]
|
, incPosttax :: [SingleAllocation PosttaxValue]
|
||||||
, incFrom :: TaggedAcnt
|
, incFrom :: TaggedAcnt
|
||||||
, incToBal :: TaggedAcnt
|
, incToBal :: TaggedAcnt
|
||||||
|
, incPayPeriod :: !Period
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deriving instance Hashable HourlyPeriod
|
||||||
|
|
||||||
|
deriving instance Hashable PeriodType
|
||||||
|
|
||||||
|
deriving instance Hashable Period
|
||||||
|
|
||||||
deriving instance Hashable Income
|
deriving instance Hashable Income
|
||||||
|
|
||||||
deriving instance (Ord w, Ord v) => Ord (Amount w v)
|
deriving instance (Ord w, Ord v) => Ord (Amount w v)
|
||||||
|
@ -747,6 +760,7 @@ data InsertError
|
||||||
| PatternError !Natural !Natural !(Maybe Natural) !PatternSuberr
|
| PatternError !Natural !Natural !(Maybe Natural) !PatternSuberr
|
||||||
| BoundsError !Gregorian !(Maybe Gregorian)
|
| BoundsError !Gregorian !(Maybe Gregorian)
|
||||||
| StatementError ![TxRecord] ![MatchRe]
|
| StatementError ![TxRecord] ![MatchRe]
|
||||||
|
| PeriodError !Day !Day
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
newtype InsertException = InsertException [InsertError]
|
newtype InsertException = InsertException [InsertError]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
module Internal.Utils
|
module Internal.Utils
|
||||||
( compareDate
|
( compareDate
|
||||||
|
, fromWeekday
|
||||||
, inBounds
|
, inBounds
|
||||||
, expandBounds
|
, expandBounds
|
||||||
, fmtRational
|
, fmtRational
|
||||||
|
@ -77,6 +78,16 @@ import Text.Regex.TDFA.Text
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- dates
|
-- dates
|
||||||
|
|
||||||
|
-- | Lame weekday converter since day of weeks aren't in dhall (yet)
|
||||||
|
fromWeekday :: Weekday -> DayOfWeek
|
||||||
|
fromWeekday Mon = Monday
|
||||||
|
fromWeekday Tue = Tuesday
|
||||||
|
fromWeekday Wed = Wednesday
|
||||||
|
fromWeekday Thu = Thursday
|
||||||
|
fromWeekday Fri = Friday
|
||||||
|
fromWeekday Sat = Saturday
|
||||||
|
fromWeekday Sun = Sunday
|
||||||
|
|
||||||
-- | find the next date
|
-- | find the next date
|
||||||
-- this is meant to go in a very tight loop and be very fast (hence no
|
-- this is meant to go in a very tight loop and be very fast (hence no
|
||||||
-- complex date functions, most of which heavily use 'mod' and friends)
|
-- complex date functions, most of which heavily use 'mod' and friends)
|
||||||
|
@ -500,7 +511,15 @@ showError other = case other of
|
||||||
, "exceed total on day"
|
, "exceed total on day"
|
||||||
, showT day
|
, showT day
|
||||||
, "where balance is"
|
, "where balance is"
|
||||||
, showT balance
|
, showT (fromRational balance :: Double)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
(PeriodError start next) ->
|
||||||
|
[ T.unwords
|
||||||
|
[ "First pay period on "
|
||||||
|
, singleQuote $ showT start
|
||||||
|
, "must start before first income payment on "
|
||||||
|
, singleQuote $ showT next
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
(BalanceError t cur rss) ->
|
(BalanceError t cur rss) ->
|
||||||
|
|
Loading…
Reference in New Issue