exchangealgebra-0.6.0.0: src/ExchangeAlgebra/Accounting/Statements/Presentation.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wincomplete-patterns -Werror=incomplete-patterns #-}
{- |
Module : ExchangeAlgebra.Accounting.Statements.Presentation
Description : Context-sensitive JGAAP presentation from trial balances.
This module is the reporting boundary between a trial balance and presented
statements. 'present' computes statements whenever a correct statement can be
computed; it does not judge whether the trial balance is normal for
accounting purposes. Abnormal-side balances (for example a deficit in
retained earnings), temporary-account residuals, reciprocal-account
mismatches, and unclosed nominal balances are reported separately by
'TB.trialBalanceFindings', and the caller decides how to treat them.
Presentation fails only when no correct statement exists: an invalid value,
a wildcard account or side, a trial balance whose debits and credits differ,
a conflicting presentation instruction, a balance on a title that no
statement section can hold, debits and credits that do not reconcile after
presentation, or a period result that cannot be derived.
Bookkeeping coordinates remain unchanged; all eliminations, relabelings,
maturity allocations, netting decisions, and subtotals are recorded as
presentation audit events.
Present a cash contribution with the standalone reporting context.
Read amounts and sections without relying on the order of statement lines.
>>> import qualified ExchangeAlgebra.Algebra.Core as Algebra
>>> import ExchangeAlgebra.Algebra.Base.Representation (Hat(..), HatBase(..))
>>> import ExchangeAlgebra.Accounting.Account.Title (AccountTitles(..))
>>> import ExchangeAlgebra.Algebra.Value (MoneyDecimal)
>>> type TrialBalance = Algebra.Alg MoneyDecimal (HatBase AccountTitles)
>>> let cash = 100 Algebra..@ (Not :< Cash) :: TrialBalance
>>> let equity = 100 Algebra..@ (Not :< CapitalStock) :: TrialBalance
>>> let capital = cash Algebra..+ equity :: TrialBalance
>>> let context = jcciSecondGradeContext Standalone
>>> let statements = present context TB.BeforeClosing capital
>>> fmap (length . _statementLines) statements
Right 2
>>> fmap (sum . map _lineAmount . _statementLines) statements == Right 200
True
>>> let isCash line = _lineAccount line == Cash
>>> let isCurrent line = _lineSection line == CurrentAssetsSection
>>> let isCurrentCash line = isCash line && isCurrent line
>>> let hasCurrentCash = any isCurrentCash . _statementLines
>>> fmap hasCurrentCash statements
Right True
-}
module ExchangeAlgebra.Accounting.Statements.Presentation
( AccountingFramework(..)
, ReportingScope(..)
, PresentationProfile(..)
, StatementSection(..)
, StatementLine(..)
, PresentationAllocation(..)
, PresentationRelabel(..)
, MaterialityTreatment(..)
, MaterialityDecision(..)
, ContraPresentationRule(..)
, CustomMetricLabel(..)
, SubtotalCoverage(..)
, SubtotalDefinition(..)
, StatementSubtotal(..)
, ReportingContext(..)
, jcciSecondGradeContext
, PresentationAuditEvent(..)
, PresentationIssue(..)
, FinancialStatements(..)
, presentationLabel
, metricLabel
, present
) where
import qualified ExchangeAlgebra.Accounting.TrialBalance.Accum as Accum
import Data.List.NonEmpty (NonEmpty(..))
import qualified Data.Map.Strict as M
import Data.Map.Strict (Map)
import qualified Data.Set as S
import Data.Set (Set)
import Data.Text (Text)
import qualified Data.Text as T
import ExchangeAlgebra.Algebra.Base.Representation (HatBase)
import ExchangeAlgebra.Algebra.Core.Representation (Alg)
import ExchangeAlgebra.Algebra.Value.Class (HatVal(..))
import ExchangeAlgebra.Accounting.Account.Classification
( AccountDivision(..)
, AccountRole(..)
, DivisionSemantics(..)
, FixedCurrent(..)
, ReportingEligibility(..)
, Side(..)
)
import ExchangeAlgebra.Accounting.Account.Registry
( AccountSemantics(asemDivisionSemantics,
asemReportingEligibility, asemRoles) )
import ExchangeAlgebra.Accounting.Account.Title (AccountTitles(..))
import qualified ExchangeAlgebra.Accounting.Account.Registry as Registry
import ExchangeAlgebra.Accounting.Statements.Metric
( DerivedMetric(..)
, MetricError
, MetricId
, PeriodResult(..)
, periodResultOf
)
import qualified ExchangeAlgebra.Accounting.TrialBalance.Validation as TB
import ExchangeAlgebra.Accounting.TrialBalance.Balance
( balanceAmount
, balanceFor
, balanceSide
)
-- | Land 4 deliberately supports JGAAP only.
data AccountingFramework = JGAAP
deriving (Show, Eq)
-- | Whether the report covers standalone or combined accounts.
data ReportingScope = Standalone | Combined
deriving (Show, Eq)
-- | Choice of statement labels and presentation profile.
data PresentationProfile
= JcciSecondGradeReport
| CanonicalJapanese
| CanonicalEnglish
deriving (Show, Eq)
-- | Section in which a statement line is displayed.
data StatementSection
= CurrentAssetsSection
| NoncurrentAssetsSection
| CurrentLiabilitiesSection
| NoncurrentLiabilitiesSection
| EquitySection
| RevenueSection
| ExpenseSection
| ContextualDebitSection
| ContextualCreditSection
deriving (Show, Eq)
-- | An account line with its label, section, side, amount, and deduction flag.
data StatementLine v = StatementLine
{ _lineAccount :: AccountTitles
, _lineLabel :: Text
, _lineSection :: StatementSection
, _lineSide :: Side
, _lineAmount :: v
, _lineIsDeduction :: Bool
}
deriving (Show, Eq)
-- | Evidence-backed split of one title without creating new bookkeeping
-- coordinates. Current and non-current parts must sum exactly to its balance.
data PresentationAllocation v = PresentationAllocation
{ _allocationAccount :: AccountTitles
, _allocationCurrent :: v
, _allocationNoncurrent :: v
, _allocationEvidence :: Text
}
deriving (Show, Eq)
-- | An account relabeling instruction with its rationale.
data PresentationRelabel = PresentationRelabel
{ _relabelSource :: AccountTitles
, _relabelTarget :: AccountTitles
, _relabelRationale :: Text
}
deriving (Show, Eq)
-- | Whether an account is presented gross, separately, or net against another.
data MaterialityTreatment
= PresentGross
| PresentSeparately
| NetAgainst AccountTitles
deriving (Show, Eq)
-- | An account materiality treatment with its rationale.
data MaterialityDecision = MaterialityDecision
{ _materialityAccount :: AccountTitles
, _materialityTreatment :: MaterialityTreatment
, _materialityRationale :: Text
}
deriving (Show, Eq)
-- | A separate or net presentation instruction for a contra account.
data ContraPresentationRule
= PresentContraSeparately AccountTitles Text
| NetContraAgainst AccountTitles AccountTitles Text
deriving (Show, Eq)
-- | Profile-specific display labels for a caller-defined metric. The metric
-- identity remains separate from these labels.
data CustomMetricLabel = CustomMetricLabel
{ _customMetricIdentity :: MetricId
, _customMetricLabelJapanese :: Text
, _customMetricLabelEnglish :: Text
}
deriving (Show, Eq)
-- | How a subtotal handles absent account titles.
data SubtotalCoverage
= RequireAllTitlesPresent
| TreatAbsentAsZero
deriving (Show, Eq)
-- | A subtotal is a typed reporting definition, never an account-basis title.
data SubtotalDefinition = SubtotalDefinition
{ _subtotalMetric :: DerivedMetric
, _subtotalCreditTitles :: [AccountTitles]
, _subtotalDebitTitles :: [AccountTitles]
, _subtotalCoverage :: SubtotalCoverage
}
deriving (Show, Eq)
-- | A reporting metric with its label and account balance.
data StatementSubtotal v = StatementSubtotal
{ _statementSubtotalMetric :: DerivedMetric
, _statementSubtotalLabel :: Text
, _statementSubtotalBalance :: TB.AccountBalance v
}
deriving (Show, Eq)
-- | Framework, scope, profile, and instructions used to present a trial balance.
data ReportingContext v = ReportingContext
{ _reportingFramework :: AccountingFramework
, _reportingScope :: ReportingScope
, _presentationProfile :: PresentationProfile
, _maturitySensitiveTitles :: Set AccountTitles
, _presentationAllocations :: [PresentationAllocation v]
, _presentationRelabels :: [PresentationRelabel]
, _materialityDecisions :: [MaterialityDecision]
, _contraPresentationRules :: [ContraPresentationRule]
, _subtotalDefinitions :: [SubtotalDefinition]
, _customMetricLabels :: [CustomMetricLabel]
}
deriving (Show, Eq)
-- | Construct a JGAAP context with the JCCI grade-2 presentation profile.
-- The caller explicitly chooses the reporting scope; it is not inferred from
-- transaction entities or periods. Maturity-sensitive titles start empty, as
-- do allocations, relabels, materiality decisions, contra rules, subtotal
-- definitions, and custom metric labels. Use record updates to supply the
-- instructions needed for the report. This starting context does not guarantee
-- that 'present' succeeds for every validated trial balance.
jcciSecondGradeContext :: ReportingScope -> ReportingContext v
jcciSecondGradeContext scope = ReportingContext
{ _reportingFramework = JGAAP
, _reportingScope = scope
, _presentationProfile = JcciSecondGradeReport
, _maturitySensitiveTitles = S.empty
, _presentationAllocations = []
, _presentationRelabels = []
, _materialityDecisions = []
, _contraPresentationRules = []
, _subtotalDefinitions = []
, _customMetricLabels = []
}
-- | A recorded presentation operation and its supporting values.
data PresentationAuditEvent v
= ReciprocalAccountsEliminated
(TB.AccountBalance v) (TB.AccountBalance v)
| AccountRelabeled
AccountTitles AccountTitles (TB.AccountBalance v) Text
| BalanceAllocated AccountTitles v v Text
| MaterialityApplied
AccountTitles MaterialityTreatment (TB.AccountBalance v) Text
| ContraPresentationApplied
AccountTitles (Maybe AccountTitles) (TB.AccountBalance v) Text
| SubtotalCalculated DerivedMetric (TB.AccountBalance v)
| LabelOverridden AccountTitles Text Text
deriving (Show, Eq)
-- | Reasons why no correct statement can be presented. Accounting
-- abnormalities that still allow a statement are not issues; they are
-- reported by 'TB.trialBalanceFindings'.
data PresentationIssue v
= InvalidStatementValue AccountTitles
-- ^ A posting value is invalid (for example NaN or infinite).
| WildcardStatementBase
-- ^ An account title or a Hat/Not side is a wildcard, so the debit or
-- credit side cannot be determined.
| UnbalancedStatementInput v v
-- ^ Debit and credit totals of the trial balance before any
-- presentation step (the per-title net balances summed on each side,
-- as in a balance trial balance). Checked before reciprocal accounts are eliminated,
-- so the elimination cannot hide an original imbalance.
| PeriodResultUnavailable MetricError
-- ^ The before-closing period result cannot be derived, so the mandatory
-- period-result subtotal cannot be presented.
| MissingPresentationAllocation AccountTitles
| DuplicatePresentationAllocation AccountTitles
| InvalidPresentationAllocation
AccountTitles (TB.AccountBalance v) v v
| BlankPresentationEvidence AccountTitles
| UnexpectedPresentationAllocation AccountTitles
| ConflictingPresentationInstruction AccountTitles
| MissingPresentationAccount AccountTitles
| BlankPresentationRationale AccountTitles
| UnpresentableBalance AccountTitles (TB.AccountBalance v)
| InvalidSubtotalDefinition DerivedMetric
| DuplicateMetricIdentity DerivedMetric
| UnlabelledCustomMetric MetricId
| UnreconciledPresentation v v
deriving (Show, Eq)
-- | Presented statement lines, subtotals, and the presentation audit.
data FinancialStatements v = FinancialStatements
{ _statementFramework :: AccountingFramework
, _statementScope :: ReportingScope
, _statementProfile :: PresentationProfile
, _statementTrialBalanceStage :: TB.TrialBalanceStage
, _statementLines :: [StatementLine v]
, _statementSubtotals :: [StatementSubtotal v]
, _presentationAudit :: [PresentationAuditEvent v]
}
deriving (Show, Eq)
-- | Profile-specific Japanese display-label overrides.
jcciSecondGradeLabelOverrides :: Map AccountTitles Text
jcciSecondGradeLabelOverrides = M.fromList
[ (AdvancesReceived, T.pack "契約負債")
]
-- | Profile-specific display label. Japanese profiles use the registry's
-- cleaned statement label, with the JCCI grade-2 overrides applied first.
presentationLabel :: PresentationProfile -> AccountTitles -> Text
presentationLabel profile title = case Registry.accountSpec title of
Nothing -> T.pack (show title)
Just spec -> case profile of
JcciSecondGradeReport -> M.findWithDefault
(Registry.asLabelJa spec) title jcciSecondGradeLabelOverrides
CanonicalJapanese -> Registry.asLabelJa spec
CanonicalEnglish -> Registry.asNameEn spec
-- | Resolve a metric's display label independently of its stable identity.
-- Profit/loss wording follows the structural balance direction.
metricLabel
:: ReportingContext v
-> DerivedMetric
-> TB.AccountBalance v
-> Maybe Text
metricLabel context metric balance = case metric of
PeriodResultMetric -> Just (builtinLabel
"当期純損益" "当期純利益" "当期純損失"
"Net result" "Net income" "Net loss")
GrossProfitMetric -> Just (builtinLabel
"売上総損益" "売上総利益" "売上総損失"
"Gross result" "Gross profit" "Gross loss")
OrdinaryProfitMetric -> Just (builtinLabel
"経常損益" "経常利益" "経常損失"
"Ordinary result" "Ordinary profit" "Ordinary loss")
CustomMetric metricId -> customLabel metricId
where
english = _presentationProfile context == CanonicalEnglish
builtinLabel neutralJa creditJa debitJa neutralEn creditEn debitEn =
case (english, balance) of
(False, TB.NoBalance) -> neutralJa
(False, TB.CreditBalance _) -> creditJa
(False, TB.DebitBalance _) -> debitJa
(True, TB.NoBalance) -> neutralEn
(True, TB.CreditBalance _) -> creditEn
(True, TB.DebitBalance _) -> debitEn
customLabel metricId = case filter
((== metricId) . _customMetricIdentity)
(_customMetricLabels context) of
[label] -> Just (if english
then _customMetricLabelEnglish label
else _customMetricLabelJapanese label)
_ -> Nothing
-- | Prepare statements using exact posting balances for transformations,
-- allocations, subtotals, and reconciliation. Each displayed amount is rounded
-- once. The sum of displayed amounts is not guaranteed to equal its subtotal.
--
-- The trial balance is a single reporting scope over account titles: select
-- an entity, period, or department and map it to account titles before calling
-- this function. The stage is stated by the caller. A 'TB.BeforeClosing' trial
-- balance always receives a 'PeriodResultMetric' subtotal derived as
-- 'periodResultOf' derives it; a caller-supplied 'PeriodResultMetric'
-- definition is rejected with 'InvalidSubtotalDefinition'. An
-- 'TB.AfterClosing' trial balance receives no period-result subtotal.
-- Presentation instructions that move a balance between a profit-and-loss
-- title and a balance-sheet title (relabels, materiality netting, contra
-- netting, and maturity allocations) are rejected with
-- 'ConflictingPresentationInstruction', so the period-result subtotal always
-- equals the exact sum of the revenue and expense balances. A maturity
-- allocation or a maturity-sensitive title must be an asset or a liability.
--
-- Titles whose balance must be split between current and noncurrent are the
-- union of '_maturitySensitiveTitles' and the implicitly maturity-sensitive
-- titles. '_maturitySensitiveTitles' lists titles whose balances require a
-- maturity split.
--
-- Success does not mean that the trial balance is normal for accounting
-- purposes. A balance on the side opposite to its home side is presented in
-- its own section on the side it actually has; inspect
-- 'TB.trialBalanceFindings' to audit it.
present
:: HatVal v
=> ReportingContext v
-> TB.TrialBalanceStage
-> Alg v (HatBase AccountTitles)
-> Either (NonEmpty (PresentationIssue v)) (FinancialStatements v)
present context stage alg = case issues of
issue : rest -> Left (issue :| rest)
[] -> Right FinancialStatements
{ _statementFramework = framework
, _statementScope = _reportingScope context
, _statementProfile = _presentationProfile context
, _statementTrialBalanceStage = stage
, _statementLines = statementLines
, _statementSubtotals = subtotals
, _presentationAudit = eliminationAudit
++ relabelAudit ++ materialityAudit ++ contraAudit
++ allocationAudit ++ subtotalAudit ++ labelAudit
}
where
framework = case _reportingFramework context of
JGAAP -> JGAAP
initialStates = Accum.accountAccums alg
initial = M.map accumulatedBalance initialStates
structuralIssues = case statementStructureIssues alg of
[] -> inputBalanceIssues initialStates
issues' -> issues'
(eliminated, eliminationAudit) = eliminateReciprocals context initialStates
(relabeled, relabelAudit) = applyRelabels context eliminated
(materialized, materialityAudit) = applyMateriality context relabeled
(transformed, contraAudit) = applyContraRules context materialized
explicitRequired = _maturitySensitiveTitles context
requiredMaturity = S.union explicitRequired
(implicitMaturityTitles displayedBalances)
displayedBalances = M.map accumulatedBalance transformed
statementLines = renderLines context displayedBalances
contextIssues = instructionIssues explicitRequired context initial
++ allocationIssues requiredMaturity context displayedBalances transformed
++ subtotalIssues context initial displayedBalances
++ coverageIssues displayedBalances
++ reconciliationIssues context transformed
issues
| not (null structuralIssues) = structuralIssues
| otherwise = contextIssues ++ periodResultIssues
(periodResultIssues, periodResultSubtotals) =
periodResultSubtotal context stage alg
allocationAudit =
[ BalanceAllocated title current noncurrent evidence
| PresentationAllocation title current noncurrent evidence
<- _presentationAllocations context
, balanceFor title displayedBalances /= TB.NoBalance
]
subtotals = periodResultSubtotals ++
[ StatementSubtotal metric label balance
| SubtotalDefinition metric credits debits _ <-
_subtotalDefinitions context
, let balance = subtotalBalance transformed credits debits
, Just label <- [metricLabel context metric balance]
]
subtotalAudit =
[ SubtotalCalculated metric balance
| StatementSubtotal metric _ balance <- subtotals
]
labelAudit =
[ LabelOverridden title canonical displayed
| _presentationProfile context == JcciSecondGradeReport
, (title, _) <- M.toList jcciSecondGradeLabelOverrides
, title `elem` map _lineAccount statementLines
, let canonical = presentationLabel CanonicalJapanese title
, let displayed = presentationLabel JcciSecondGradeReport title
, canonical /= displayed
]
eliminateReciprocals
:: HatVal v => ReportingContext v
-> Map AccountTitles (Accum v)
-> (Map AccountTitles (Accum v), [PresentationAuditEvent v])
eliminateReciprocals context balances = case _reportingScope context of
Standalone -> (balances, [])
Combined ->
( M.delete BranchCurrentAccount
(M.delete HeadOfficeCurrentAccount balances)
, [ ReciprocalAccountsEliminated branch headOffice
| branch /= TB.NoBalance || headOffice /= TB.NoBalance
]
)
where
branch = accumulatedBalance (accumulatedFor BranchCurrentAccount balances)
headOffice = accumulatedBalance (accumulatedFor HeadOfficeCurrentAccount balances)
applyRelabels
:: HatVal v
=> ReportingContext v
-> Map AccountTitles (Accum v)
-> (Map AccountTitles (Accum v), [PresentationAuditEvent v])
applyRelabels context balances0 =
Prelude.foldl applyOne (balances0, []) (_presentationRelabels context)
where
applyOne (balances, events) (PresentationRelabel source target rationale) =
let sourceBalance = accumulatedBalance (accumulatedFor source balances)
targetBalance = accumulatedFor target balances
next = M.insert target (mergeAccum targetBalance (accumulatedFor source balances))
(M.delete source balances)
in (next, events ++
[AccountRelabeled source target sourceBalance rationale])
applyMateriality
:: HatVal v
=> ReportingContext v
-> Map AccountTitles (Accum v)
-> (Map AccountTitles (Accum v), [PresentationAuditEvent v])
applyMateriality context balances0 =
Prelude.foldl applyOne (balances0, []) (_materialityDecisions context)
where
applyOne (balances, events) decision =
let source = _materialityAccount decision
treatment = _materialityTreatment decision
next = case treatment of
PresentGross -> balances
PresentSeparately -> balances
NetAgainst target -> moveBalance source target balances
event = MaterialityApplied source treatment
(accumulatedBalance (accumulatedFor source balances))
(_materialityRationale decision)
in (next, events ++ [event])
applyContraRules
:: HatVal v
=> ReportingContext v
-> Map AccountTitles (Accum v)
-> (Map AccountTitles (Accum v), [PresentationAuditEvent v])
applyContraRules context balances0 =
Prelude.foldl applyOne (balances0, [])
(_contraPresentationRules context)
where
applyOne (balances, events) rule = case rule of
PresentContraSeparately source rationale ->
(balances, events ++ [ContraPresentationApplied source Nothing
(accumulatedBalance (accumulatedFor source balances)) rationale])
NetContraAgainst source target rationale ->
( moveBalance source target balances
, events ++ [ContraPresentationApplied source (Just target)
(accumulatedBalance (accumulatedFor source balances)) rationale]
)
moveBalance
:: HatVal v
=> AccountTitles -> AccountTitles
-> Map AccountTitles (Accum v)
-> Map AccountTitles (Accum v)
moveBalance source target balances =
if source == target
then balances
else M.insert target
(mergeAccum (accumulatedFor target balances)
(accumulatedFor source balances))
(M.delete source balances)
allocationIssues
:: HatVal v
=> Set AccountTitles
-> ReportingContext v
-> Map AccountTitles (TB.AccountBalance v)
-> Map AccountTitles (Accum v)
-> [PresentationIssue v]
allocationIssues required context balances states = concatMap checkTitle checkedTitles
++ [ DuplicatePresentationAllocation title
| title <- duplicateTitles (map _allocationAccount allocations)
]
++ map UnexpectedPresentationAllocation
(S.toList (allocationTitles `S.difference` required))
where
allocations = _presentationAllocations context
allocationTitles = S.fromList (map _allocationAccount allocations)
checkedTitles = S.toList (required `S.union` allocationTitles)
checkTitle title
| Just semantics <- Registry.accountSemantics title
, StatementDivision division <- asemDivisionSemantics semantics
, division `elem` [Assets, Liability] = checkBalance title
| otherwise = [ConflictingPresentationInstruction title]
checkBalance title = case balanceFor title balances of
TB.NoBalance
| title `S.member` allocationTitles ->
[MissingPresentationAccount title]
| otherwise -> []
balance -> case filter ((== title) . _allocationAccount) allocations of
[] -> [MissingPresentationAllocation title]
[allocation]
| T.null (T.strip (_allocationEvidence allocation)) ->
[BlankPresentationEvidence title]
| allocationMatches (accumulatedFor title states) allocation -> []
| otherwise -> [InvalidPresentationAllocation title balance
(_allocationCurrent allocation)
(_allocationNoncurrent allocation)]
_ -> []
allocationMatches
:: HatVal v
=> Accum v -> PresentationAllocation v -> Bool
allocationMatches total allocation =
_allocationCurrent allocation >= 0
&& _allocationNoncurrent allocation >= 0
&& signAccum (mergeAccum allocated (negateAccum magnitude)) == Just EQ
where
magnitude
| signAccum total == Just LT = negateAccum total
| otherwise = total
allocated = addAccum (_allocationCurrent allocation)
(addAccum (_allocationNoncurrent allocation) emptyAccum)
instructionIssues
:: HatVal v
=> Set AccountTitles
-> ReportingContext v
-> Map AccountTitles (TB.AccountBalance v)
-> [PresentationIssue v]
instructionIssues explicitRequired context balances =
duplicateInstructionIssues
++ concatMap relabelIssue (_presentationRelabels context)
++ concatMap materialityIssue (_materialityDecisions context)
++ concatMap contraIssue (_contraPresentationRules context)
where
duplicateInstructionIssues =
map ConflictingPresentationInstruction
(duplicateTitles instructionSources)
++ [ ConflictingPresentationInstruction title
| _reportingScope context == Combined
, title <- [BranchCurrentAccount, HeadOfficeCurrentAccount]
, title `elem` instructionSources
]
instructionSources = concatMap relabelTitles (_presentationRelabels context)
++ concatMap materialityTitles (_materialityDecisions context)
++ concatMap contraTitles (_contraPresentationRules context)
relabelIssue rule
| T.null (T.strip (_relabelRationale rule)) =
[BlankPresentationRationale (_relabelSource rule)]
| balanceFor (_relabelSource rule) balances == TB.NoBalance =
[MissingPresentationAccount (_relabelSource rule)]
| _relabelSource rule `S.member` explicitRequired =
[ConflictingPresentationInstruction (_relabelSource rule)]
| isNominal (_relabelSource rule) /= isNominal (_relabelTarget rule) =
[ConflictingPresentationInstruction (_relabelSource rule)]
| not (presentableTarget (_relabelTarget rule)) =
[ConflictingPresentationInstruction (_relabelTarget rule)]
| otherwise = []
materialityIssue decision
| T.null (T.strip (_materialityRationale decision)) =
[BlankPresentationRationale (_materialityAccount decision)]
| balanceFor (_materialityAccount decision) balances == TB.NoBalance =
[MissingPresentationAccount (_materialityAccount decision)]
| otherwise = case _materialityTreatment decision of
NetAgainst target
| _materialityAccount decision `S.member` explicitRequired ->
[ConflictingPresentationInstruction
(_materialityAccount decision)]
| target == _materialityAccount decision ->
[ConflictingPresentationInstruction target]
| isNominal (_materialityAccount decision) /= isNominal target ->
[ConflictingPresentationInstruction (_materialityAccount decision)]
| not (presentableTarget target) ->
[ConflictingPresentationInstruction target]
| M.notMember target balances ->
[MissingPresentationAccount target]
_ -> []
contraIssue rule = case rule of
PresentContraSeparately source rationale
| T.null (T.strip rationale) ->
[BlankPresentationRationale source]
| balanceFor source balances == TB.NoBalance ->
[MissingPresentationAccount source]
| otherwise -> []
NetContraAgainst source target rationale
| T.null (T.strip rationale) ->
[BlankPresentationRationale source]
| balanceFor source balances == TB.NoBalance ->
[MissingPresentationAccount source]
| source `S.member` explicitRequired ->
[ConflictingPresentationInstruction source]
| source == target ->
[ConflictingPresentationInstruction source]
| isNominal source /= isNominal target ->
[ConflictingPresentationInstruction source]
| not (presentableTarget target) ->
[ConflictingPresentationInstruction target]
| M.notMember target balances ->
[MissingPresentationAccount target]
| otherwise -> []
isNominal title = case Registry.accountSemantics title of
Just semantics -> asemDivisionSemantics semantics `elem`
[StatementDivision Revenue, StatementDivision Cost]
Nothing -> False
relabelTitles rule = [_relabelSource rule, _relabelTarget rule]
materialityTitles decision = _materialityAccount decision : case
_materialityTreatment decision of
NetAgainst target -> [target]
PresentGross -> []
PresentSeparately -> []
contraTitles (PresentContraSeparately source _) = [source]
contraTitles (NetContraAgainst source target _) = [source, target]
duplicateTitles :: [AccountTitles] -> [AccountTitles]
duplicateTitles titles = S.toList
(S.fromList [title | title <- titles, count title titles > 1])
where
count needle = length . filter (== needle)
presentableTarget :: AccountTitles -> Bool
presentableTarget title = case Registry.accountSemantics title of
Just semantics -> asemReportingEligibility semantics `elem`
[StatementEligible, ContextualPresentation]
Nothing -> False
implicitMaturityTitles
:: Map AccountTitles (TB.AccountBalance v) -> Set AccountTitles
implicitMaturityTitles balances = S.fromList
[ title
| (title, balance) <- M.toList balances
, balanceSide balance /= Side
, fixedCurrent title == Other
, Just semantics <- [Registry.accountSemantics title]
, StatementDivision division <- [asemDivisionSemantics semantics]
, division `elem` [Assets, Liability]
]
-- | Invalid values and wildcard bases, which leave the debit or credit side
-- undetermined. When this list is non-empty, no other check is evaluated,
-- because the account accumulation relies on a determined side.
statementStructureIssues
:: HatVal v
=> Alg v (HatBase AccountTitles)
-> [PresentationIssue v]
statementStructureIssues alg =
map (InvalidStatementValue . fst) invalidValues
++ map (const WildcardStatementBase) (wildcardAccounts ++ wildcardSides)
where
(invalidValues, wildcardAccounts, wildcardSides) = Accum.structureErrors alg
-- | Exact debit and credit totals of the trial balance before elimination,
-- relabeling, netting, or allocation; empty when they are equal.
inputBalanceIssues
:: HatVal v
=> Map AccountTitles (Accum v)
-> [PresentationIssue v]
inputBalanceIssues states =
[ UnbalancedStatementInput (roundAccum debit) (roundAccum credit)
| signAccum (mergeAccum debit (negateAccum credit)) /= Just EQ ]
where
(debit, credit) = M.foldl' collect (emptyAccum, emptyAccum) states
collect (debits, credits) total = case signAccum total of
Just GT -> (mergeAccum debits total, credits)
Just LT -> (debits, mergeAccum credits (negateAccum total))
_ -> (debits, credits)
-- | The mandatory period-result subtotal of a before-closing trial balance,
-- derived as 'periodResultOf' derives it, or the issue that prevents it. An
-- after-closing trial balance yields neither.
periodResultSubtotal
:: HatVal v
=> ReportingContext v
-> TB.TrialBalanceStage
-> Alg v (HatBase AccountTitles)
-> ([PresentationIssue v], [StatementSubtotal v])
periodResultSubtotal _ TB.AfterClosing _ = ([], [])
periodResultSubtotal context TB.BeforeClosing alg = case periodResultOf TB.BeforeClosing alg of
Left issue -> ([PeriodResultUnavailable issue], [])
Right result ->
let balance = case result of
PeriodProfit value -> TB.CreditBalance value
PeriodLoss value -> TB.DebitBalance value
PeriodBreakEven -> TB.NoBalance
in ([], [StatementSubtotal PeriodResultMetric label balance
| Just label <- [metricLabel context PeriodResultMetric balance]])
coverageIssues
:: Map AccountTitles (TB.AccountBalance v) -> [PresentationIssue v]
coverageIssues balances =
[ UnpresentableBalance title balance
| (title, balance) <- M.toList balances
, balanceSide balance /= Side
, not (presentableTarget title)
]
reconciliationIssues
:: HatVal v
=> ReportingContext v -> Map AccountTitles (Accum v) -> [PresentationIssue v]
reconciliationIssues context balances =
[ UnreconciledPresentation (roundAccum debit) (roundAccum credit)
| signAccum (mergeAccum debit (negateAccum credit)) /= Just EQ ]
where
(debit, credit) = M.foldlWithKey' collect (emptyAccum, emptyAccum) balances
collect totals@(debits, credits) title total
| not (hasAllocation title)
&& reportingSection title (accumulatedBalance total) == Nothing = totals
| otherwise = case signAccum total of
Just GT -> (mergeAccum debits (allocated title total), credits)
Just LT -> (debits, mergeAccum credits (allocated title (negateAccum total)))
_ -> (debits, credits)
hasAllocation title = any ((== title) . _allocationAccount)
(_presentationAllocations context)
allocated title total = case filter ((== title) . _allocationAccount)
(_presentationAllocations context) of
allocation : _ -> addAccum (_allocationCurrent allocation)
(addAccum (_allocationNoncurrent allocation) emptyAccum)
[] -> total
subtotalIssues
:: HatVal v
=> ReportingContext v
-> Map AccountTitles (TB.AccountBalance v)
-> Map AccountTitles (TB.AccountBalance v)
-> [PresentationIssue v]
subtotalIssues context initial balances = duplicateDefinitionIssues
++ duplicateCustomLabelIssues
++ concatMap check definitions
where
definitions = _subtotalDefinitions context
definitionMetrics = map _subtotalMetric definitions
duplicateDefinitionIssues =
[ DuplicateMetricIdentity metric
| metric <- duplicateMetrics definitionMetrics
]
customLabelIds = map _customMetricIdentity (_customMetricLabels context)
duplicateCustomLabelIssues =
[ DuplicateMetricIdentity (CustomMetric metricId)
| metricId <- duplicateMetricIds customLabelIds
]
check (SubtotalDefinition metric credits debits coverage)
| metric == PeriodResultMetric = [InvalidSubtotalDefinition metric]
| null credits && null debits = [InvalidSubtotalDefinition metric]
| not (null (duplicateTitles (credits ++ debits))) =
[InvalidSubtotalDefinition metric]
| customMetricUnlabelled metric = [customMetricIssue metric]
| coverage == RequireAllTitlesPresent
&& any (`M.notMember` balances) (credits ++ debits) =
[InvalidSubtotalDefinition metric]
| coverage == TreatAbsentAsZero
&& any removedNonZeroTitle (credits ++ debits) =
[InvalidSubtotalDefinition metric]
| any (wrongSide Credit coverage) credits =
[InvalidSubtotalDefinition metric]
| any (wrongSide Debit coverage) debits =
[InvalidSubtotalDefinition metric]
| otherwise = []
removedNonZeroTitle title =
balanceFor title initial /= TB.NoBalance && M.notMember title balances
wrongSide expected coverage title = case balanceFor title balances of
TB.NoBalance -> coverage == RequireAllTitlesPresent
balance -> balanceSide balance /= expected
customMetricUnlabelled metric = case metric of
CustomMetric metricId -> case customLabels metricId of
[label] -> T.null (T.strip (_customMetricLabelJapanese label))
|| T.null (T.strip (_customMetricLabelEnglish label))
_ -> True
_ -> False
customMetricIssue metric = case metric of
CustomMetric metricId -> UnlabelledCustomMetric metricId
_ -> InvalidSubtotalDefinition metric
customLabels metricId = filter
((== metricId) . _customMetricIdentity)
(_customMetricLabels context)
duplicateMetrics :: [DerivedMetric] -> [DerivedMetric]
duplicateMetrics metrics = S.toList
(S.fromList [metric | metric <- metrics, count metric metrics > 1])
where
count needle = length . filter (== needle)
duplicateMetricIds :: [MetricId] -> [MetricId]
duplicateMetricIds metricIds = S.toList
(S.fromList [metricId | metricId <- metricIds, count metricId metricIds > 1])
where
count needle = length . filter (== needle)
renderLines
:: (Eq v, Num v)
=> ReportingContext v
-> Map AccountTitles (TB.AccountBalance v)
-> [StatementLine v]
renderLines context balances = concatMap renderOne (M.toList balances)
where
renderOne (_, TB.NoBalance) = []
renderOne (title, balance) = case allocationFor title of
Just allocation -> allocatedLines context title balance allocation
Nothing -> case reportingSection title balance of
Nothing -> []
Just section -> [lineFor context title section balance]
allocationFor title = case filter ((== title) . _allocationAccount)
(_presentationAllocations context) of
allocation : _ -> Just allocation
[] -> Nothing
allocatedLines
:: (Eq v, Num v)
=> ReportingContext v -> AccountTitles -> TB.AccountBalance v
-> PresentationAllocation v -> [StatementLine v]
allocatedLines context title balance allocation =
[ StatementLine title (presentationLabel (_presentationProfile context) title)
section side amount (isContraTitle title)
| (section, amount) <-
[ (currentSection title balance, _allocationCurrent allocation)
, (noncurrentSection title balance, _allocationNoncurrent allocation)
]
, amount /= 0
]
where
side = balanceSide balance
lineFor
:: Num v => ReportingContext v -> AccountTitles -> StatementSection
-> TB.AccountBalance v -> StatementLine v
lineFor context title section balance = StatementLine
title (presentationLabel (_presentationProfile context) title)
section (balanceSide balance) (balanceAmount balance) (isContraTitle title)
isContraTitle :: AccountTitles -> Bool
isContraTitle title = case Registry.accountSemantics title of
Just semantics -> ContraAccount `elem` asemRoles semantics
Nothing -> False
reportingSection
:: AccountTitles -> TB.AccountBalance v -> Maybe StatementSection
reportingSection title balance = do
semantics <- Registry.accountSemantics title
case asemReportingEligibility semantics of
DerivedPresentation -> Nothing
NotPresented -> Nothing
ContextualPresentation -> Just (contextualSection balance)
StatementEligible -> case asemDivisionSemantics semantics of
StatementDivision division -> Just (divisionSection title division)
_ -> Nothing
divisionSection :: AccountTitles -> AccountDivision -> StatementSection
divisionSection title division = case division of
Assets -> case fixedCurrent title of
Fixed -> NoncurrentAssetsSection
_ -> CurrentAssetsSection
Liability -> case fixedCurrent title of
Fixed -> NoncurrentLiabilitiesSection
_ -> CurrentLiabilitiesSection
Equity -> EquitySection
Revenue -> RevenueSection
Cost -> ExpenseSection
fixedCurrent :: AccountTitles -> FixedCurrent
fixedCurrent title = case Registry.accountSpec title of
Just spec -> Registry.asFixedCurrent spec
Nothing -> Other
contextualSection :: TB.AccountBalance v -> StatementSection
contextualSection (TB.DebitBalance _) = ContextualDebitSection
contextualSection (TB.CreditBalance _) = ContextualCreditSection
contextualSection TB.NoBalance = ContextualDebitSection
currentSection :: AccountTitles -> TB.AccountBalance v -> StatementSection
currentSection title balance = case Registry.accountSemantics title of
Just semantics -> case asemDivisionSemantics semantics of
StatementDivision Liability -> CurrentLiabilitiesSection
StatementDivision Assets -> CurrentAssetsSection
_ -> contextualSection balance
Nothing -> contextualSection balance
noncurrentSection :: AccountTitles -> TB.AccountBalance v -> StatementSection
noncurrentSection title balance = case Registry.accountSemantics title of
Just semantics -> case asemDivisionSemantics semantics of
StatementDivision Liability -> NoncurrentLiabilitiesSection
StatementDivision Assets -> NoncurrentAssetsSection
_ -> contextualSection balance
Nothing -> contextualSection balance
-- | Read one exact signed balance as a structural side and rounded magnitude.
accumulatedBalance :: HatVal v => Accum v -> TB.AccountBalance v
accumulatedBalance total = case Accum.readAccountAccum Credit total of
(Side, _) -> TB.NoBalance
(Debit, value) -> TB.DebitBalance value
(Credit, value) -> TB.CreditBalance value
-- | An absent title contributes the empty exact sum.
accumulatedFor :: HatVal v => AccountTitles -> Map AccountTitles (Accum v) -> Accum v
accumulatedFor = M.findWithDefault emptyAccum
subtotalBalance
:: HatVal v
=> Map AccountTitles (Accum v)
-> [AccountTitles] -> [AccountTitles] -> TB.AccountBalance v
subtotalBalance balances creditTitles debitTitles = accumulatedBalance
(foldr (mergeAccum . (`accumulatedFor` balances)) emptyAccum
(debitTitles ++ creditTitles))