jaz-cli 2.3.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/skills/api/SKILL.md +35 -34
- package/assets/skills/api/references/errors.md +15 -7
- package/assets/skills/api/references/feature-glossary.md +2 -0
- package/assets/skills/api/references/field-map.md +3 -3
- package/assets/skills/conversion/SKILL.md +1 -1
- package/assets/skills/transaction-recipes/SKILL.md +158 -14
- package/assets/skills/transaction-recipes/references/asset-disposal.md +174 -0
- package/assets/skills/transaction-recipes/references/bad-debt-provision.md +145 -0
- package/assets/skills/transaction-recipes/references/building-blocks.md +25 -2
- package/assets/skills/transaction-recipes/references/capital-wip.md +167 -0
- package/assets/skills/transaction-recipes/references/dividend.md +111 -0
- package/assets/skills/transaction-recipes/references/employee-accruals.md +154 -0
- package/assets/skills/transaction-recipes/references/fixed-deposit.md +164 -0
- package/assets/skills/transaction-recipes/references/fx-revaluation.md +135 -0
- package/assets/skills/transaction-recipes/references/hire-purchase.md +190 -0
- package/assets/skills/transaction-recipes/references/intercompany.md +150 -0
- package/assets/skills/transaction-recipes/references/provisions.md +142 -0
- package/dist/calc/amortization.js +122 -0
- package/dist/calc/asset-disposal.js +151 -0
- package/dist/calc/blueprint.js +46 -0
- package/dist/calc/depreciation.js +200 -0
- package/dist/calc/ecl.js +101 -0
- package/dist/calc/fixed-deposit.js +169 -0
- package/dist/calc/format.js +494 -0
- package/dist/calc/fx-reval.js +93 -0
- package/dist/calc/lease.js +146 -0
- package/dist/calc/loan.js +107 -0
- package/dist/calc/provision.js +128 -0
- package/dist/calc/types.js +21 -0
- package/dist/calc/validate.js +48 -0
- package/dist/commands/calc.js +252 -0
- package/dist/index.js +2 -0
- package/package.json +3 -2
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Recipe: Provisions with PV Unwinding (IAS 37)
|
|
2
|
+
|
|
3
|
+
## Scenario
|
|
4
|
+
|
|
5
|
+
Your company has a product warranty obligation. Based on historical claims data, you estimate $500,000 in warranty costs over the next 5 years. Because the time value of money is material (IAS 37.45), you recognize the provision at its present value using a 4% discount rate. Each month, the discount unwinds as a finance cost, gradually increasing the provision balance until it reaches the full $500,000 at settlement.
|
|
6
|
+
|
|
7
|
+
**Pattern:** Manual journals + capsule (interest amounts change each period, same as loan/lease unwinding)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Accounts Involved
|
|
12
|
+
|
|
13
|
+
| Account | Type | Subtype | Role |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| Provision Expense | Expense | Expense | Initial recognition at PV |
|
|
16
|
+
| Provision for Obligations | Liability | Non-Current Liability | Holds the discounted obligation |
|
|
17
|
+
| Finance Cost — Unwinding | Expense | Expense | Monthly discount unwinding charge |
|
|
18
|
+
| Cash / Bank Account | Asset | Bank | Settlement when obligation is paid |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Journal Entries
|
|
23
|
+
|
|
24
|
+
### Step 1: Initial Recognition (at PV)
|
|
25
|
+
|
|
26
|
+
| Line | Account | Debit | Credit |
|
|
27
|
+
|---|---|---|---|
|
|
28
|
+
| 1 | Provision Expense | *present value* | |
|
|
29
|
+
| 2 | Provision for Obligations | | *present value* |
|
|
30
|
+
|
|
31
|
+
### Step 2: Monthly Unwinding (each month)
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Interest = Opening Provision Balance × Monthly Rate
|
|
35
|
+
Monthly Rate = Annual Rate / 12
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| Line | Account | Debit | Credit |
|
|
39
|
+
|---|---|---|---|
|
|
40
|
+
| 1 | Finance Cost — Unwinding | *interest amount* | |
|
|
41
|
+
| 2 | Provision for Obligations | | *interest amount* |
|
|
42
|
+
|
|
43
|
+
The provision balance grows each month until it reaches the nominal amount at settlement.
|
|
44
|
+
|
|
45
|
+
### Step 3: Settlement (when obligation is paid)
|
|
46
|
+
|
|
47
|
+
| Line | Account | Debit | Credit |
|
|
48
|
+
|---|---|---|---|
|
|
49
|
+
| 1 | Provision for Obligations | *nominal amount* | |
|
|
50
|
+
| 2 | Cash / Bank Account | | *actual amount paid* |
|
|
51
|
+
| 3 | Provision Expense (or Revenue) | difference | (if actual ≠ estimate) |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Capsule Structure
|
|
56
|
+
|
|
57
|
+
**Capsule:** "Warranty Provision — Product X — 2025"
|
|
58
|
+
**Capsule Type:** "Provisions"
|
|
59
|
+
|
|
60
|
+
Contents:
|
|
61
|
+
- 1 initial recognition journal
|
|
62
|
+
- 60 monthly unwinding journals (5 years)
|
|
63
|
+
- Settlement journal(s) when claims are paid
|
|
64
|
+
- Revision journals if estimate changes
|
|
65
|
+
- **Total entries:** 62+
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Worked Example
|
|
70
|
+
|
|
71
|
+
**Setup:**
|
|
72
|
+
- Estimated future outflow: $500,000
|
|
73
|
+
- Discount rate: 4% annual (0.333% monthly)
|
|
74
|
+
- Settlement term: 60 months
|
|
75
|
+
- Present value: $409,501.55
|
|
76
|
+
|
|
77
|
+
**Calculation:**
|
|
78
|
+
```
|
|
79
|
+
PV = 500,000 / (1 + 0.04/12)^60 = $409,501.55
|
|
80
|
+
Total unwinding = $500,000 − $409,501.55 = $90,498.45
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Jan 1, 2025 — Initial recognition:**
|
|
84
|
+
- Dr Provision Expense $409,501.55
|
|
85
|
+
- Cr Provision for Obligations $409,501.55
|
|
86
|
+
- Description: "Initial provision recognition at PV (IAS 37)"
|
|
87
|
+
|
|
88
|
+
**Jan 31, 2025 — Month 1 unwinding:**
|
|
89
|
+
- Opening balance: $409,501.55
|
|
90
|
+
- Interest: $409,501.55 × 0.00333 = $1,365.01
|
|
91
|
+
- Closing balance: $410,866.56
|
|
92
|
+
- Dr Finance Cost — Unwinding $1,365.01
|
|
93
|
+
- Cr Provision for Obligations $1,365.01
|
|
94
|
+
|
|
95
|
+
**Feb 28, 2025 — Month 2 unwinding:**
|
|
96
|
+
- Opening balance: $410,866.56
|
|
97
|
+
- Interest: $410,866.56 × 0.00333 = $1,369.56
|
|
98
|
+
- Closing balance: $412,236.12
|
|
99
|
+
|
|
100
|
+
**Dec 31, 2029 — Month 60 (final):**
|
|
101
|
+
- Closing balance reaches exactly $500,000.00
|
|
102
|
+
|
|
103
|
+
**Settlement:**
|
|
104
|
+
- Dr Provision for Obligations $500,000
|
|
105
|
+
- Cr Cash $500,000
|
|
106
|
+
|
|
107
|
+
**Use the calculator:** `jaz calc provision --amount 500000 --rate 4 --term 60 --start-date 2025-01-01`
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Enrichment Suggestions
|
|
112
|
+
|
|
113
|
+
| Enrichment | Value | Why |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| Tracking Tag | "Provision" | Filter all provision-related entries |
|
|
116
|
+
| Tracking Tag | "IAS 37" | Mark IFRS-specific adjustments |
|
|
117
|
+
| Custom Field | "Obligation Type" → "Warranty" | Identify the type of provision |
|
|
118
|
+
| Custom Field | "Expected Settlement Date" → "2029-12-31" | Record the estimated settlement date |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Verification
|
|
123
|
+
|
|
124
|
+
1. **Trial Balance at Jan 31** → Provision for Obligations shows $410,866.56 credit. Finance Cost shows $1,365.01.
|
|
125
|
+
2. **Trial Balance at Dec 31, Year 1** → Provision balance should match the year-end closing balance from the unwinding schedule (~$426,185).
|
|
126
|
+
3. **Final period** → Provision balance reaches exactly $500,000.00 (the calculator's final-period adjustment ensures this).
|
|
127
|
+
4. **P&L per year** → Finance Cost — Unwinding increases each year (compounding effect), totaling $90,498.45 over 5 years.
|
|
128
|
+
5. **Group General Ledger by Capsule** → Complete history from recognition through every unwinding entry.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Variations
|
|
133
|
+
|
|
134
|
+
**Revision of estimate:** If the estimated outflow changes (e.g., from $500,000 to $550,000), adjust the provision and recalculate the unwinding schedule. The change in estimate goes to Provision Expense, not Finance Cost.
|
|
135
|
+
|
|
136
|
+
**Short-term provision (no discounting):** If settlement is within 12 months, PV discounting is not required (IAS 37.45 — "effect of time value not material"). Simply recognize at the nominal amount with no unwinding schedule.
|
|
137
|
+
|
|
138
|
+
**Decommissioning provision:** Same pattern, but the initial recognition is capitalized to the related asset (Dr Asset / Cr Provision) instead of expensed. The asset is then depreciated over its useful life.
|
|
139
|
+
|
|
140
|
+
**Legal claim provision:** If the outcome is uncertain, provision is recognized only when "probable" (>50% likelihood). If only "possible," disclose as a contingent liability but do not recognize. Use the provision calculator once the amount and timing are estimable.
|
|
141
|
+
|
|
142
|
+
**Multiple payment dates:** If the obligation will be settled in tranches (e.g., warranty claims over 5 years), model as multiple provisions or use a weighted average settlement period.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prepaid expense & deferred revenue recognition calculators.
|
|
3
|
+
* Equal-period division. Final period absorbs rounding remainder.
|
|
4
|
+
*/
|
|
5
|
+
import { round2, addMonths } from './types.js';
|
|
6
|
+
import { validatePositive, validatePositiveInteger, validateDateFormat } from './validate.js';
|
|
7
|
+
import { journalStep, billStep, invoiceStep, fmtCapsuleAmount, fmtAmt } from './blueprint.js';
|
|
8
|
+
export function calculatePrepaidExpense(inputs) {
|
|
9
|
+
const { amount, periods, frequency = 'monthly', startDate, currency } = inputs;
|
|
10
|
+
validatePositive(amount, 'Amount');
|
|
11
|
+
validatePositiveInteger(periods, 'Periods');
|
|
12
|
+
validateDateFormat(startDate);
|
|
13
|
+
const schedule = buildSchedule(amount, periods, frequency, startDate, 'prepaid');
|
|
14
|
+
let blueprint = null;
|
|
15
|
+
if (startDate) {
|
|
16
|
+
const steps = [
|
|
17
|
+
billStep(1, 'Create bill from supplier coded to Prepaid Asset, then pay from Cash / Bank Account', startDate, [
|
|
18
|
+
{ account: 'Prepaid Asset', debit: amount, credit: 0 },
|
|
19
|
+
{ account: 'Cash / Bank Account', debit: 0, credit: amount },
|
|
20
|
+
]),
|
|
21
|
+
...schedule.map((row, idx) => journalStep(idx + 2, row.journal.description, row.date, row.journal.lines)),
|
|
22
|
+
];
|
|
23
|
+
const c = currency ?? undefined;
|
|
24
|
+
const workings = [
|
|
25
|
+
`Prepaid Expense Recognition Workings`,
|
|
26
|
+
`Total prepaid: ${fmtAmt(amount, c)} | Periods: ${periods} (${frequency})`,
|
|
27
|
+
`Per period: ${fmtAmt(round2(amount / periods), c)}`,
|
|
28
|
+
`Method: Straight-line recognition over ${periods} ${frequency} periods`,
|
|
29
|
+
`Rounding: 2dp per period, final period absorbs remainder`,
|
|
30
|
+
].join('\n');
|
|
31
|
+
blueprint = {
|
|
32
|
+
capsuleType: 'Prepaid Expenses',
|
|
33
|
+
capsuleName: `Prepaid Expense — ${fmtCapsuleAmount(amount, currency)} — ${periods} periods`,
|
|
34
|
+
capsuleDescription: workings,
|
|
35
|
+
tags: ['Prepaid Expense'],
|
|
36
|
+
customFields: { 'Policy / Contract #': null },
|
|
37
|
+
steps,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
type: 'prepaid-expense',
|
|
42
|
+
currency: currency ?? null,
|
|
43
|
+
inputs: { amount, periods, frequency, startDate: startDate ?? null },
|
|
44
|
+
perPeriodAmount: round2(amount / periods),
|
|
45
|
+
schedule,
|
|
46
|
+
blueprint,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function calculateDeferredRevenue(inputs) {
|
|
50
|
+
const { amount, periods, frequency = 'monthly', startDate, currency } = inputs;
|
|
51
|
+
validatePositive(amount, 'Amount');
|
|
52
|
+
validatePositiveInteger(periods, 'Periods');
|
|
53
|
+
validateDateFormat(startDate);
|
|
54
|
+
const schedule = buildSchedule(amount, periods, frequency, startDate, 'deferred');
|
|
55
|
+
let blueprint = null;
|
|
56
|
+
if (startDate) {
|
|
57
|
+
const steps = [
|
|
58
|
+
invoiceStep(1, 'Create invoice to customer coded to Deferred Revenue, record payment to Cash / Bank Account', startDate, [
|
|
59
|
+
{ account: 'Cash / Bank Account', debit: amount, credit: 0 },
|
|
60
|
+
{ account: 'Deferred Revenue', debit: 0, credit: amount },
|
|
61
|
+
]),
|
|
62
|
+
...schedule.map((row, idx) => journalStep(idx + 2, row.journal.description, row.date, row.journal.lines)),
|
|
63
|
+
];
|
|
64
|
+
const c2 = currency ?? undefined;
|
|
65
|
+
const workings2 = [
|
|
66
|
+
`Deferred Revenue Recognition Workings`,
|
|
67
|
+
`Total deferred: ${fmtAmt(amount, c2)} | Periods: ${periods} (${frequency})`,
|
|
68
|
+
`Per period: ${fmtAmt(round2(amount / periods), c2)}`,
|
|
69
|
+
`Method: Straight-line recognition over ${periods} ${frequency} periods`,
|
|
70
|
+
`Rounding: 2dp per period, final period absorbs remainder`,
|
|
71
|
+
].join('\n');
|
|
72
|
+
blueprint = {
|
|
73
|
+
capsuleType: 'Deferred Revenue',
|
|
74
|
+
capsuleName: `Deferred Revenue — ${fmtCapsuleAmount(amount, currency)} — ${periods} periods`,
|
|
75
|
+
capsuleDescription: workings2,
|
|
76
|
+
tags: ['Deferred Revenue'],
|
|
77
|
+
customFields: { 'Contract #': null },
|
|
78
|
+
steps,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
type: 'deferred-revenue',
|
|
83
|
+
currency: currency ?? null,
|
|
84
|
+
inputs: { amount, periods, frequency, startDate: startDate ?? null },
|
|
85
|
+
perPeriodAmount: round2(amount / periods),
|
|
86
|
+
schedule,
|
|
87
|
+
blueprint,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function buildSchedule(amount, periods, frequency, startDate, kind) {
|
|
91
|
+
const perPeriod = round2(amount / periods);
|
|
92
|
+
const schedule = [];
|
|
93
|
+
let remaining = amount;
|
|
94
|
+
const monthsPerPeriod = frequency === 'quarterly' ? 3 : 1;
|
|
95
|
+
for (let i = 1; i <= periods; i++) {
|
|
96
|
+
const isFinal = i === periods;
|
|
97
|
+
const amortized = isFinal ? round2(remaining) : perPeriod;
|
|
98
|
+
remaining = round2(remaining - amortized);
|
|
99
|
+
const date = startDate ? addMonths(startDate, i * monthsPerPeriod) : null;
|
|
100
|
+
let journal;
|
|
101
|
+
if (kind === 'prepaid') {
|
|
102
|
+
journal = {
|
|
103
|
+
description: `Prepaid expense recognition — Period ${i} of ${periods}`,
|
|
104
|
+
lines: [
|
|
105
|
+
{ account: 'Expense', debit: amortized, credit: 0 },
|
|
106
|
+
{ account: 'Prepaid Asset', debit: 0, credit: amortized },
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
journal = {
|
|
112
|
+
description: `Deferred revenue recognition — Period ${i} of ${periods}`,
|
|
113
|
+
lines: [
|
|
114
|
+
{ account: 'Deferred Revenue', debit: amortized, credit: 0 },
|
|
115
|
+
{ account: 'Revenue', debit: 0, credit: amortized },
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
schedule.push({ period: i, date, amortized, remainingBalance: remaining, journal });
|
|
120
|
+
}
|
|
121
|
+
return schedule;
|
|
122
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset disposal gain/loss calculator.
|
|
3
|
+
*
|
|
4
|
+
* Compliance references:
|
|
5
|
+
* - IAS 16.67-72: Derecognition on disposal or when no future economic benefits expected
|
|
6
|
+
* - IAS 16.68: Gain/loss = Net disposal proceeds - Carrying amount
|
|
7
|
+
* - IAS 16.71: Gain ≠ revenue (classified separately in P&L)
|
|
8
|
+
*
|
|
9
|
+
* Computes depreciation to the disposal date (pro-rated partial period),
|
|
10
|
+
* then calculates NBV and gain/loss. Supports SL, DDB, and 150DB methods.
|
|
11
|
+
*/
|
|
12
|
+
import { round2 } from './types.js';
|
|
13
|
+
import { CalcValidationError, validatePositive, validateNonNegative, validatePositiveInteger, validateSalvageLessThanCost, validateDateFormat, } from './validate.js';
|
|
14
|
+
import { journalStep, noteStep, fmtCapsuleAmount, fmtAmt } from './blueprint.js';
|
|
15
|
+
/**
|
|
16
|
+
* Count full months between two YYYY-MM-DD dates.
|
|
17
|
+
* Partial months are rounded up (any day in a month counts as a full month).
|
|
18
|
+
*/
|
|
19
|
+
function monthsBetween(from, to) {
|
|
20
|
+
const d1 = new Date(from + 'T00:00:00');
|
|
21
|
+
const d2 = new Date(to + 'T00:00:00');
|
|
22
|
+
const months = (d2.getFullYear() - d1.getFullYear()) * 12 + (d2.getMonth() - d1.getMonth());
|
|
23
|
+
// If disposal day > acquisition day, it's a partial month — round up
|
|
24
|
+
return d2.getDate() >= d1.getDate() ? months + 1 : Math.max(months, 0);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Compute accumulated depreciation from acquisition to disposal date.
|
|
28
|
+
*/
|
|
29
|
+
function computeAccumDepreciation(cost, salvageValue, usefulLifeYears, method, monthsHeld) {
|
|
30
|
+
const totalMonths = usefulLifeYears * 12;
|
|
31
|
+
const depreciableBase = round2(cost - salvageValue);
|
|
32
|
+
// Cap at useful life — asset is fully depreciated
|
|
33
|
+
const effectiveMonths = Math.min(monthsHeld, totalMonths);
|
|
34
|
+
if (method === 'sl') {
|
|
35
|
+
// Straight-line: pro-rate by months
|
|
36
|
+
const monthlyDep = depreciableBase / totalMonths;
|
|
37
|
+
return round2(Math.min(monthlyDep * effectiveMonths, depreciableBase));
|
|
38
|
+
}
|
|
39
|
+
// DDB / 150DB: compute year-by-year then pro-rate final partial year
|
|
40
|
+
const multiplier = method === 'ddb' ? 2 : 1.5;
|
|
41
|
+
const annualRate = multiplier / usefulLifeYears;
|
|
42
|
+
let bookValue = cost;
|
|
43
|
+
let totalDep = 0;
|
|
44
|
+
let monthsRemaining = effectiveMonths;
|
|
45
|
+
for (let year = 1; year <= usefulLifeYears && monthsRemaining > 0; year++) {
|
|
46
|
+
const remainingYears = usefulLifeYears - year + 1;
|
|
47
|
+
const ddbAmount = round2(bookValue * annualRate);
|
|
48
|
+
const slAmount = round2((bookValue - salvageValue) / remainingYears);
|
|
49
|
+
const useSL = slAmount >= ddbAmount || round2(bookValue - ddbAmount) < salvageValue;
|
|
50
|
+
let annualDep = useSL ? slAmount : ddbAmount;
|
|
51
|
+
// Don't breach salvage floor
|
|
52
|
+
if (round2(bookValue - annualDep) < salvageValue) {
|
|
53
|
+
annualDep = round2(bookValue - salvageValue);
|
|
54
|
+
}
|
|
55
|
+
if (monthsRemaining >= 12) {
|
|
56
|
+
// Full year
|
|
57
|
+
totalDep = round2(totalDep + annualDep);
|
|
58
|
+
bookValue = round2(bookValue - annualDep);
|
|
59
|
+
monthsRemaining -= 12;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Partial year: pro-rate
|
|
63
|
+
const partialDep = round2(annualDep * monthsRemaining / 12);
|
|
64
|
+
totalDep = round2(totalDep + partialDep);
|
|
65
|
+
monthsRemaining = 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return Math.min(totalDep, depreciableBase);
|
|
69
|
+
}
|
|
70
|
+
export function calculateAssetDisposal(inputs) {
|
|
71
|
+
const { cost, salvageValue, usefulLifeYears, acquisitionDate, disposalDate, proceeds, method = 'sl', currency, } = inputs;
|
|
72
|
+
validatePositive(cost, 'Asset cost');
|
|
73
|
+
validateNonNegative(salvageValue, 'Salvage value');
|
|
74
|
+
validatePositiveInteger(usefulLifeYears, 'Useful life (years)');
|
|
75
|
+
if (salvageValue > 0)
|
|
76
|
+
validateSalvageLessThanCost(salvageValue, cost);
|
|
77
|
+
validateNonNegative(proceeds, 'Disposal proceeds');
|
|
78
|
+
validateDateFormat(acquisitionDate);
|
|
79
|
+
validateDateFormat(disposalDate);
|
|
80
|
+
// Validate disposal is after acquisition
|
|
81
|
+
if (disposalDate <= acquisitionDate) {
|
|
82
|
+
throw new CalcValidationError('Disposal date must be after acquisition date.');
|
|
83
|
+
}
|
|
84
|
+
const monthsHeld = monthsBetween(acquisitionDate, disposalDate);
|
|
85
|
+
const accumulatedDepreciation = computeAccumDepreciation(cost, salvageValue, usefulLifeYears, method, monthsHeld);
|
|
86
|
+
const netBookValue = round2(cost - accumulatedDepreciation);
|
|
87
|
+
const gainOrLoss = round2(proceeds - netBookValue);
|
|
88
|
+
const isGain = gainOrLoss >= 0;
|
|
89
|
+
// Build disposal journal entry
|
|
90
|
+
const lines = [];
|
|
91
|
+
if (proceeds > 0) {
|
|
92
|
+
lines.push({ account: 'Cash / Bank Account', debit: proceeds, credit: 0 });
|
|
93
|
+
}
|
|
94
|
+
if (accumulatedDepreciation > 0) {
|
|
95
|
+
lines.push({ account: 'Accumulated Depreciation', debit: accumulatedDepreciation, credit: 0 });
|
|
96
|
+
}
|
|
97
|
+
if (!isGain) {
|
|
98
|
+
lines.push({ account: 'Loss on Disposal', debit: Math.abs(gainOrLoss), credit: 0 });
|
|
99
|
+
}
|
|
100
|
+
lines.push({ account: 'Fixed Asset (at cost)', debit: 0, credit: cost });
|
|
101
|
+
if (isGain && gainOrLoss > 0) {
|
|
102
|
+
lines.push({ account: 'Gain on Disposal', debit: 0, credit: gainOrLoss });
|
|
103
|
+
}
|
|
104
|
+
const disposalJournal = {
|
|
105
|
+
description: `Asset disposal — ${isGain ? (gainOrLoss > 0 ? 'gain' : 'at book value') : 'loss'}`,
|
|
106
|
+
lines,
|
|
107
|
+
};
|
|
108
|
+
// Build blueprint
|
|
109
|
+
const c = currency ?? undefined;
|
|
110
|
+
const methodLabel = method === 'sl' ? 'Straight-line' : method === 'ddb' ? 'Double declining' : '150% declining';
|
|
111
|
+
const workings = [
|
|
112
|
+
`Asset Disposal Workings (IAS 16)`,
|
|
113
|
+
`Cost: ${fmtAmt(cost, c)} | Salvage: ${fmtAmt(salvageValue, c)} | Life: ${usefulLifeYears} years (${methodLabel})`,
|
|
114
|
+
`Acquired: ${acquisitionDate} | Disposed: ${disposalDate} | Held: ${monthsHeld} months`,
|
|
115
|
+
`Accumulated depreciation: ${fmtAmt(accumulatedDepreciation, c)} | NBV: ${fmtAmt(netBookValue, c)}`,
|
|
116
|
+
`Proceeds: ${fmtAmt(proceeds, c)} | ${isGain ? (gainOrLoss > 0 ? `Gain: ${fmtAmt(gainOrLoss, c)}` : 'At book value (no gain/loss)') : `Loss: ${fmtAmt(Math.abs(gainOrLoss), c)}`}`,
|
|
117
|
+
`Method: IAS 16.68 — Gain/Loss = Proceeds − NBV = ${fmtAmt(proceeds, c)} − ${fmtAmt(netBookValue, c)} = ${fmtAmt(gainOrLoss, c)}`,
|
|
118
|
+
].join('\n');
|
|
119
|
+
let blueprint = null;
|
|
120
|
+
blueprint = {
|
|
121
|
+
capsuleType: 'Asset Disposal',
|
|
122
|
+
capsuleName: `Asset Disposal — ${fmtCapsuleAmount(cost, currency)} asset — ${disposalDate}`,
|
|
123
|
+
capsuleDescription: workings,
|
|
124
|
+
tags: ['Asset Disposal'],
|
|
125
|
+
customFields: { 'Asset Description': null },
|
|
126
|
+
steps: [
|
|
127
|
+
journalStep(1, disposalJournal.description, disposalDate, disposalJournal.lines),
|
|
128
|
+
noteStep(2, `Update Jaz FA register: use POST /mark-as-sold/fixed-assets (if sold) or POST /discard-fixed-assets/:id (if scrapped).`, disposalDate),
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
type: 'asset-disposal',
|
|
133
|
+
currency: currency ?? null,
|
|
134
|
+
inputs: {
|
|
135
|
+
cost,
|
|
136
|
+
salvageValue,
|
|
137
|
+
usefulLifeYears,
|
|
138
|
+
acquisitionDate,
|
|
139
|
+
disposalDate,
|
|
140
|
+
proceeds,
|
|
141
|
+
method,
|
|
142
|
+
},
|
|
143
|
+
monthsHeld,
|
|
144
|
+
accumulatedDepreciation,
|
|
145
|
+
netBookValue,
|
|
146
|
+
gainOrLoss,
|
|
147
|
+
isGain,
|
|
148
|
+
disposalJournal,
|
|
149
|
+
blueprint,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blueprint builder — translates calculator output into a
|
|
3
|
+
* capsule + transaction execution plan for Jaz.
|
|
4
|
+
*
|
|
5
|
+
* The blueprint gives an AI agent or human everything needed to
|
|
6
|
+
* create the capsule and post all journal entries in Jaz without
|
|
7
|
+
* any manual translation from the schedule.
|
|
8
|
+
*/
|
|
9
|
+
/** Build a blueprint step from a journal entry with a date. */
|
|
10
|
+
export function journalStep(stepNum, description, date, lines) {
|
|
11
|
+
return { step: stepNum, action: 'journal', description, date, lines };
|
|
12
|
+
}
|
|
13
|
+
/** Build a bill step (create supplier bill, e.g. prepaid expense). Lines show net accounting effect. */
|
|
14
|
+
export function billStep(stepNum, description, date, lines) {
|
|
15
|
+
return { step: stepNum, action: 'bill', description, date, lines };
|
|
16
|
+
}
|
|
17
|
+
/** Build an invoice step (create customer invoice, e.g. deferred revenue). Lines show net accounting effect. */
|
|
18
|
+
export function invoiceStep(stepNum, description, date, lines) {
|
|
19
|
+
return { step: stepNum, action: 'invoice', description, date, lines };
|
|
20
|
+
}
|
|
21
|
+
/** Build a cash-out step (cash disbursement, e.g. deposit placement). */
|
|
22
|
+
export function cashOutStep(stepNum, description, date, lines) {
|
|
23
|
+
return { step: stepNum, action: 'cash-out', description, date, lines };
|
|
24
|
+
}
|
|
25
|
+
/** Build a cash-in step (cash receipt, e.g. deposit maturity). */
|
|
26
|
+
export function cashInStep(stepNum, description, date, lines) {
|
|
27
|
+
return { step: stepNum, action: 'cash-in', description, date, lines };
|
|
28
|
+
}
|
|
29
|
+
/** Build a fixed-asset registration step (e.g. register ROU asset in FA module). */
|
|
30
|
+
export function fixedAssetStep(stepNum, description, date = null) {
|
|
31
|
+
return { step: stepNum, action: 'fixed-asset', description, date, lines: [] };
|
|
32
|
+
}
|
|
33
|
+
/** Build a note step (instruction, not a journal — e.g. "update FA register"). */
|
|
34
|
+
export function noteStep(stepNum, description, date = null) {
|
|
35
|
+
return { step: stepNum, action: 'note', description, date, lines: [] };
|
|
36
|
+
}
|
|
37
|
+
/** Format a currency amount for capsule names (e.g. "SGD 100,000"). */
|
|
38
|
+
export function fmtCapsuleAmount(amount, currency) {
|
|
39
|
+
const formatted = amount.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 });
|
|
40
|
+
return currency ? `${currency} ${formatted}` : formatted;
|
|
41
|
+
}
|
|
42
|
+
/** Format a currency amount for workings text (e.g. "SGD 100,000.00"). */
|
|
43
|
+
export function fmtAmt(amount, currency) {
|
|
44
|
+
const formatted = amount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
45
|
+
return currency ? `${currency} ${formatted}` : formatted;
|
|
46
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Depreciation schedule calculator.
|
|
3
|
+
*
|
|
4
|
+
* Methods:
|
|
5
|
+
* - sl: Straight-line (cost - salvage) / life
|
|
6
|
+
* - ddb: Double declining balance (2 / life), auto switch-to-SL
|
|
7
|
+
* - 150db: 150% declining balance (1.5 / life), auto switch-to-SL
|
|
8
|
+
*
|
|
9
|
+
* Book value never goes below salvage value.
|
|
10
|
+
* Total depreciation always equals (cost - salvage).
|
|
11
|
+
*/
|
|
12
|
+
import { round2 } from './types.js';
|
|
13
|
+
import { validatePositive, validateNonNegative, validatePositiveInteger, validateSalvageLessThanCost } from './validate.js';
|
|
14
|
+
import { journalStep, fmtAmt } from './blueprint.js';
|
|
15
|
+
export function calculateDepreciation(inputs) {
|
|
16
|
+
const { cost, salvageValue, usefulLifeYears, method = 'ddb', frequency = 'annual', currency, } = inputs;
|
|
17
|
+
validatePositive(cost, 'Asset cost');
|
|
18
|
+
validateNonNegative(salvageValue, 'Salvage value');
|
|
19
|
+
validatePositiveInteger(usefulLifeYears, 'Useful life (years)');
|
|
20
|
+
if (salvageValue > 0)
|
|
21
|
+
validateSalvageLessThanCost(salvageValue, cost);
|
|
22
|
+
const depreciableBase = round2(cost - salvageValue);
|
|
23
|
+
if (method === 'sl') {
|
|
24
|
+
return buildStraightLineResult(cost, salvageValue, usefulLifeYears, depreciableBase, frequency, currency);
|
|
25
|
+
}
|
|
26
|
+
const multiplier = method === 'ddb' ? 2 : 1.5;
|
|
27
|
+
const annualRate = multiplier / usefulLifeYears;
|
|
28
|
+
if (frequency === 'annual') {
|
|
29
|
+
return buildDecliningSchedule(cost, salvageValue, usefulLifeYears, annualRate, depreciableBase, method, currency);
|
|
30
|
+
}
|
|
31
|
+
// Monthly: compute annual schedule then subdivide each year into 12 equal months
|
|
32
|
+
const annual = buildDecliningSchedule(cost, salvageValue, usefulLifeYears, annualRate, depreciableBase, method, currency);
|
|
33
|
+
const monthlySchedule = [];
|
|
34
|
+
let periodNum = 0;
|
|
35
|
+
for (const yearRow of annual.schedule) {
|
|
36
|
+
const monthlyAmount = round2(yearRow.depreciation / 12);
|
|
37
|
+
let yearRemaining = yearRow.depreciation;
|
|
38
|
+
for (let m = 1; m <= 12; m++) {
|
|
39
|
+
periodNum++;
|
|
40
|
+
const isLastMonth = m === 12;
|
|
41
|
+
const dep = isLastMonth ? round2(yearRemaining) : monthlyAmount;
|
|
42
|
+
yearRemaining = round2(yearRemaining - dep);
|
|
43
|
+
const openingBV = periodNum === 1
|
|
44
|
+
? cost
|
|
45
|
+
: round2(monthlySchedule[periodNum - 2].closingBookValue);
|
|
46
|
+
const closingBV = round2(openingBV - dep);
|
|
47
|
+
const journal = {
|
|
48
|
+
description: `${yearRow.methodUsed} depreciation — Month ${periodNum} of ${usefulLifeYears * 12}`,
|
|
49
|
+
lines: [
|
|
50
|
+
{ account: 'Depreciation Expense', debit: dep, credit: 0 },
|
|
51
|
+
{ account: 'Accumulated Depreciation', debit: 0, credit: dep },
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
monthlySchedule.push({
|
|
55
|
+
period: periodNum,
|
|
56
|
+
date: null,
|
|
57
|
+
openingBookValue: openingBV,
|
|
58
|
+
ddbAmount: round2(yearRow.ddbAmount / 12),
|
|
59
|
+
slAmount: round2(yearRow.slAmount / 12),
|
|
60
|
+
methodUsed: yearRow.methodUsed,
|
|
61
|
+
depreciation: dep,
|
|
62
|
+
closingBookValue: closingBV,
|
|
63
|
+
journal,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
type: 'depreciation',
|
|
69
|
+
currency: currency ?? null,
|
|
70
|
+
inputs: { cost, salvageValue, usefulLifeYears, method, frequency },
|
|
71
|
+
totalDepreciation: depreciableBase,
|
|
72
|
+
schedule: monthlySchedule,
|
|
73
|
+
blueprint: annual.blueprint, // reuse annual blueprint (monthly is just subdivision)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function buildStraightLineResult(cost, salvageValue, usefulLifeYears, depreciableBase, frequency, currency) {
|
|
77
|
+
const totalPeriods = frequency === 'monthly' ? usefulLifeYears * 12 : usefulLifeYears;
|
|
78
|
+
const perPeriod = round2(depreciableBase / totalPeriods);
|
|
79
|
+
const periodLabel = frequency === 'monthly' ? 'Month' : 'Year';
|
|
80
|
+
const totalLabel = frequency === 'monthly' ? usefulLifeYears * 12 : usefulLifeYears;
|
|
81
|
+
const schedule = [];
|
|
82
|
+
let bookValue = cost;
|
|
83
|
+
for (let i = 1; i <= totalPeriods; i++) {
|
|
84
|
+
const openingBV = round2(bookValue);
|
|
85
|
+
const isFinal = i === totalPeriods;
|
|
86
|
+
const dep = isFinal ? round2(openingBV - salvageValue) : perPeriod;
|
|
87
|
+
bookValue = round2(openingBV - dep);
|
|
88
|
+
const journal = {
|
|
89
|
+
description: `SL depreciation — ${periodLabel} ${i} of ${totalLabel}`,
|
|
90
|
+
lines: [
|
|
91
|
+
{ account: 'Depreciation Expense', debit: dep, credit: 0 },
|
|
92
|
+
{ account: 'Accumulated Depreciation', debit: 0, credit: dep },
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
schedule.push({
|
|
96
|
+
period: i,
|
|
97
|
+
date: null,
|
|
98
|
+
openingBookValue: openingBV,
|
|
99
|
+
ddbAmount: 0,
|
|
100
|
+
slAmount: dep,
|
|
101
|
+
methodUsed: 'SL',
|
|
102
|
+
depreciation: dep,
|
|
103
|
+
closingBookValue: bookValue,
|
|
104
|
+
journal,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const blueprintSteps = schedule.map((row, idx) => journalStep(idx + 1, row.journal.description, row.date, row.journal.lines));
|
|
108
|
+
const c = currency ?? undefined;
|
|
109
|
+
const workings = [
|
|
110
|
+
`Straight-Line Depreciation Workings`,
|
|
111
|
+
`Cost: ${fmtAmt(cost, c)} | Salvage: ${fmtAmt(salvageValue, c)} | Depreciable base: ${fmtAmt(depreciableBase, c)}`,
|
|
112
|
+
`Useful life: ${usefulLifeYears} years | Frequency: ${frequency}`,
|
|
113
|
+
`Per ${frequency === 'monthly' ? 'month' : 'year'}: ${fmtAmt(perPeriod, c)}`,
|
|
114
|
+
`Total depreciation: ${fmtAmt(depreciableBase, c)}`,
|
|
115
|
+
`Method: (Cost − Salvage) ÷ Life = ${fmtAmt(depreciableBase, c)} ÷ ${totalPeriods} = ${fmtAmt(perPeriod, c)}`,
|
|
116
|
+
`Rounding: 2dp per period, final period closes to salvage value`,
|
|
117
|
+
].join('\n');
|
|
118
|
+
return {
|
|
119
|
+
type: 'depreciation',
|
|
120
|
+
currency: currency ?? null,
|
|
121
|
+
inputs: { cost, salvageValue, usefulLifeYears, method: 'sl', frequency },
|
|
122
|
+
totalDepreciation: depreciableBase,
|
|
123
|
+
schedule,
|
|
124
|
+
blueprint: {
|
|
125
|
+
capsuleType: 'Depreciation',
|
|
126
|
+
capsuleName: `SL Depreciation — ${usefulLifeYears} years`,
|
|
127
|
+
capsuleDescription: workings,
|
|
128
|
+
tags: ['Depreciation'],
|
|
129
|
+
customFields: { 'Asset ID': null },
|
|
130
|
+
steps: blueprintSteps,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function buildDecliningSchedule(cost, salvageValue, usefulLifeYears, annualRate, depreciableBase, method, currency) {
|
|
135
|
+
const schedule = [];
|
|
136
|
+
let bookValue = cost;
|
|
137
|
+
for (let year = 1; year <= usefulLifeYears; year++) {
|
|
138
|
+
const openingBV = round2(bookValue);
|
|
139
|
+
const remainingYears = usefulLifeYears - year + 1;
|
|
140
|
+
// DDB amount (rate × opening book value)
|
|
141
|
+
const ddbAmount = round2(openingBV * annualRate);
|
|
142
|
+
// SL for remaining life: (book value - salvage) / remaining years
|
|
143
|
+
const slAmount = round2((openingBV - salvageValue) / remainingYears);
|
|
144
|
+
// Switch to SL when SL >= DDB, or when DDB would push below salvage
|
|
145
|
+
const ddbCapped = round2(openingBV - ddbAmount) < salvageValue;
|
|
146
|
+
const useSL = slAmount >= ddbAmount || ddbCapped;
|
|
147
|
+
const methodUsed = useSL ? 'SL' : (method === '150db' ? '150DB' : 'DDB');
|
|
148
|
+
// Depreciation cannot push book value below salvage
|
|
149
|
+
let depreciation = useSL ? slAmount : ddbAmount;
|
|
150
|
+
if (round2(openingBV - depreciation) < salvageValue) {
|
|
151
|
+
depreciation = round2(openingBV - salvageValue);
|
|
152
|
+
}
|
|
153
|
+
bookValue = round2(openingBV - depreciation);
|
|
154
|
+
const journal = {
|
|
155
|
+
description: `${methodUsed} depreciation — Year ${year} of ${usefulLifeYears}`,
|
|
156
|
+
lines: [
|
|
157
|
+
{ account: 'Depreciation Expense', debit: depreciation, credit: 0 },
|
|
158
|
+
{ account: 'Accumulated Depreciation', debit: 0, credit: depreciation },
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
schedule.push({
|
|
162
|
+
period: year,
|
|
163
|
+
date: null,
|
|
164
|
+
openingBookValue: openingBV,
|
|
165
|
+
ddbAmount,
|
|
166
|
+
slAmount,
|
|
167
|
+
methodUsed,
|
|
168
|
+
depreciation,
|
|
169
|
+
closingBookValue: bookValue,
|
|
170
|
+
journal,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const blueprintSteps = schedule.map((row, idx) => journalStep(idx + 1, row.journal.description, row.date, row.journal.lines));
|
|
174
|
+
const c = currency ?? undefined;
|
|
175
|
+
const methodLabel = method === 'ddb' ? 'Double Declining Balance' : '150% Declining Balance';
|
|
176
|
+
const ratePercent = round2(annualRate * 100);
|
|
177
|
+
const workings = [
|
|
178
|
+
`${methodLabel} Depreciation Workings`,
|
|
179
|
+
`Cost: ${fmtAmt(cost, c)} | Salvage: ${fmtAmt(salvageValue, c)} | Depreciable base: ${fmtAmt(depreciableBase, c)}`,
|
|
180
|
+
`Useful life: ${usefulLifeYears} years | Rate: ${ratePercent}% (${method === 'ddb' ? '2' : '1.5'} ÷ ${usefulLifeYears})`,
|
|
181
|
+
`Total depreciation: ${fmtAmt(depreciableBase, c)}`,
|
|
182
|
+
`Method: ${method.toUpperCase()} with auto switch to SL when SL ≥ declining or floor hit`,
|
|
183
|
+
`Rounding: 2dp per period, book value never falls below salvage`,
|
|
184
|
+
].join('\n');
|
|
185
|
+
return {
|
|
186
|
+
type: 'depreciation',
|
|
187
|
+
currency: currency ?? null,
|
|
188
|
+
inputs: { cost, salvageValue, usefulLifeYears, method, frequency: 'annual' },
|
|
189
|
+
totalDepreciation: depreciableBase,
|
|
190
|
+
schedule,
|
|
191
|
+
blueprint: {
|
|
192
|
+
capsuleType: 'Depreciation',
|
|
193
|
+
capsuleName: `${method.toUpperCase()} Depreciation — ${usefulLifeYears} years`,
|
|
194
|
+
capsuleDescription: workings,
|
|
195
|
+
tags: ['Depreciation'],
|
|
196
|
+
customFields: { 'Asset ID': null },
|
|
197
|
+
steps: blueprintSteps,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|