Files
tax-scripts/fifo/index.js
2025-03-28 08:25:58 -06:00

193 lines
5.9 KiB
JavaScript

const csvToJson = require('convert-csv-to-json')
const converter = require('json-2-csv')
const fs = require('fs')
const BN = require('bignumber.js')
const { PDFDocument } = require('pdf-lib')
const year = 2024
const txs = csvToJson.fieldDelimiter(',').getJsonFromCsv(`../Ledger.csv`)
let lots = []
let dispositions = []
const handleBuy = (tx, i) => {
const { Date: date, Asset, Received, Price } = tx
lots.push({
Date: date,
Id: i,
Asset,
Amount: Received,
Price,
Remaining: new BN(Received),
})
}
const createDisposition = (tx, lot, amount) => {
const st = new Date(tx.Date).valueOf() - new Date(lot.Date).valueOf() < 31556926000
const costBasis = amount.times(lot.Price)
const proceeds = amount.times(tx.Price)
const gainLoss = proceeds.minus(costBasis)
const disposition = {
Date: tx.Date,
Asset: tx.Asset,
Lot: lot.Id,
Amount: amount.toFixed(8),
PurchasePrice: lot.Price,
DateAcquired: lot.Date,
SalePrice: tx.Price,
CostBasis: costBasis.toFixed(2),
Proceeds: proceeds.toFixed(2),
GainLoss: gainLoss.toFixed(2),
ShortTerm: st ? 'true' : '',
Tax: gainLoss.gt(0) ? gainLoss.times(st ? .24 : .15).toFixed(2) : 0
}
dispositions.push(disposition)
}
const consumeLots = (tx) => {
for (let i = 0; i < lots.length; i++) {
let lot = lots[i]
if (!lot) {
console.error(`WARNING: No lot available! Assuming $0 cost basis:\n-Date:${tx.Date}\n-Asset: ${tx.Asset}\n-Sent: ${tx.Sent}\n-Remaining: ${tx.Remaining.toFixed(8)}\n`)
lot = {
Date: '2017-01-01',
Id: -1,
Asset: tx.Asset,
Amount: tx.Remaining.toFixed(8),
Price: '0',
Remaining: tx.Remaining,
}
}
if (lot.Asset !== tx.Asset || lot.Remaining.isZero()) continue
if (lot.Remaining.gte(tx.Remaining)) {
createDisposition(tx, lot, tx.Remaining)
lot.Remaining = lot.Remaining.minus(tx.Remaining)
return
} else {
createDisposition(tx, lot, lot.Remaining)
tx.Remaining = tx.Remaining.minus(lot.Remaining)
lot.Remaining = new BN(0)
return consumeLots(tx)
}
}
}
txs.forEach((tx, i) => {
if (tx.Received) return handleBuy(tx, i)
tx.Remaining = new BN(tx.Sent)
return consumeLots(tx)
})
let balances = lots.reduce((prev, curr) => {
const asset = curr.Asset
if (prev[asset]) {
prev[asset] = prev[asset].plus(curr.Remaining)
} else {
prev[asset] = curr.Remaining
}
return prev
}, {})
lots = lots.map(u => ({
...u,
Remaining: u.Remaining.toFixed(8),
}))
balances = Object.keys(balances)
.filter(asset => !balances[asset].isZero())
.map(asset => ({
Asset: asset,
balance: balances[asset].toFixed(8)
}))
converter.json2csv(lots).then(csv =>
fs.appendFileSync(`./lots.csv`, csv)
)
converter.json2csv(dispositions).then(csv =>
fs.appendFileSync(`./dispositions.csv`, csv)
)
converter.json2csv(balances).then(csv =>
fs.appendFileSync(`./balances.csv`, csv)
)
// **** 8949 ****
const getDate = (date) => {
const dateObj = new Date(date)
const month = dateObj.getMonth() + 1
const day = dateObj.getDate()
const year = dateObj.getFullYear()
return month + "/" + day + "/" + year;
}
const fillFormRows = (form, page, disposition, i) => {
const dateAcquired = getDate(disposition.DateAcquired)
const dateSold = getDate(disposition.Date)
const name = `topmostSubform[0].Page${page}[0].Table_Line1[0].Row${i + 1}[0].f${page}_`
form.getTextField(`${name}${i * 8 + 3}[0]`).setText(`${disposition.Amount} ${disposition.Asset}`)
form.getTextField(`${name}${i * 8 + 4}[0]`).setText(dateAcquired)
form.getTextField(`${name}${i * 8 + 5}[0]`).setText(dateSold)
form.getTextField(`${name}${i * 8 + 6}[0]`).setText(disposition.Proceeds.toString())
form.getTextField(`${name}${i * 8 + 7}[0]`).setText(disposition.CostBasis.toString())
form.getTextField(`${name}${i * 8 + 10}[0]`).setText(disposition.GainLoss.toString())
}
const fillFormTotals = (form, page, proceeds, basis) => {
form.getTextField(`topmostSubform[0].Page${page}[0].f${page}_115[0]`).setText(proceeds.toFixed(2))
form.getTextField(`topmostSubform[0].Page${page}[0].f${page}_116[0]`).setText(basis.toFixed(2))
form.getTextField(`topmostSubform[0].Page${page}[0].f${page}_119[0]`).setText(proceeds.minus(basis).toFixed(2))
}
const createPDF = async () => {
const pdfDoc = await PDFDocument.create()
const stDispositions = dispositions.filter(d => d.ShortTerm && new Date(d.Date).getFullYear() === year)
const ltDispositions = dispositions.filter(d => !d.ShortTerm && new Date(d.Date).getFullYear() === year)
const maxTxs = Math.max(stDispositions.length, ltDispositions.length)
const numPages = Math.ceil(maxTxs / 14)
for (let i = 0; i < numPages; i++) {
const blank8949Bytes = fs.readFileSync('../' + year + '-f8949.pdf')
const blank8949 = await PDFDocument.load(blank8949Bytes)
const form = blank8949.getForm()
const start = i * 14
// short term
const st = stDispositions.slice(start, start + 14)
const stTotals = st.reduce((totals, d, i) => {
fillFormRows(form, 1, d, i)
totals.proceeds = totals.proceeds.plus(d.Proceeds)
totals.basis = totals.basis.plus(d.CostBasis)
return totals
}, { proceeds: new BN(0), basis: new BN(0) })
fillFormTotals(form, 1, stTotals.proceeds, stTotals.basis)
// long term
const lt = ltDispositions.slice(start, start + 14)
const ltTotals = lt.reduce((totals, d, i) => {
fillFormRows(form, 2, d, i)
totals.proceeds = totals.proceeds.plus(d.Proceeds)
totals.basis = totals.basis.plus(d.CostBasis)
return totals
}, { proceeds: new BN(0), basis: new BN(0) })
fillFormTotals(form, 2, ltTotals.proceeds, ltTotals.basis)
await blank8949.save()
const [page1, page2] = await pdfDoc.copyPages(blank8949, [0,1])
pdfDoc.addPage(page1)
pdfDoc.addPage(page2)
}
const pdfBytes = await pdfDoc.save()
fs.writeFileSync(`./8949.pdf`, pdfBytes)
}
createPDF()