diff --git a/kasownik_classifier/.gitignore b/kasownik_classifier/.gitignore new file mode 100644 index 0000000..7209fbb --- /dev/null +++ b/kasownik_classifier/.gitignore @@ -0,0 +1,3 @@ +node_modules +raw_transfers.json +raw_transfers.txt diff --git a/kasownik_classifier/2021-finances.txt b/kasownik_classifier/2021-finances.txt new file mode 100644 index 0000000..645d42c --- /dev/null +++ b/kasownik_classifier/2021-finances.txt @@ -0,0 +1,59 @@ +Finanse w 2021, wszystko podane w przeliczeniu na miesiąc + +Koszty stałe: +- czynsz, woda, prąd, ogrzewanie: -7095 PLN +- bgp.wtf (nitronet, epix, ripe): -1201 PLN +- księgowość: -984 PLN += 9280/ms + +Przychody główne: +- składki członkowskie: 9645 PLN +- przychody bgp.wtf: 1002 PLN += 10647/ms + +Pozostałe przychody: +- granty: 1667 +- darowizny: 1335 +- darowizny celowe: 611 += 3613/ms + +Pozostałe koszty: +- wydatki związane z grantmi: 2304 +- prawnicy: 210 +- zakupy (budowlane): 181 +- zakupy (materiały): 317 +- zakupy (pozostałe): 955 +- zakupy (zwroty): 285 +- wydatki nieskategoryzowane: 660 +- opłaty bankowe: 11 += 4923/ms + +balance: +57/ms + +———————————————————————— + +czynsz: + +2017: 5119/mo +2018: 5394/mo +2019: 5458/mo +2020: 6457/mo +2021: 7094/mo +38% vs 2017 + +———————————————————————— + +składki członkowskie: + +2017: 6987/mo +2018: 8227/mo +2019: 7730/mo +2020: 8905/mo +2021: 9644/mo +38% vs 2017 + +———————————————————————— + +przychody bgp.wtf: + +2019: 507/mo +2020: 361/mo +2021: 1002/mo diff --git a/kasownik_classifier/analyze.js b/kasownik_classifier/analyze.js new file mode 100644 index 0000000..58e98e9 --- /dev/null +++ b/kasownik_classifier/analyze.js @@ -0,0 +1,285 @@ +const fs = require('fs') +const fp = require('rambdax') + +let rawTransfers = JSON.parse(fs.readFileSync('raw_transfers.json').toString('utf8')) +rawTransfers = fp.piped(rawTransfers, fp.sortByPath('date')) + +const pad = (chars, text, padding = ' ') => { + return `${text}${Array(Math.max(chars - text.length, 0)).join(padding)}` +} +const padRight = (chars, text, padding = ' ') => { + return `${Array(Math.max(chars - text.length, 0) + 1).join(padding)}${text}` +} +const formatAmmount = (amount) => { + const zl = amount >= 100 ? String(amount).slice(0, -2) : '0' + const gr = String(amount).slice(-2) + return zl + ',' + padRight(2, gr, '0') +} +const toPln = (currency) => { + switch (currency) { + case 'PLN': + return 1.0 + case 'EUR': + return 4.57 + default: + throw new Error('Unknown currency multiplier for ' + currency) + } +} +const sign = (transfer) => { + return transfer.type.startsWith('IN') ? 1 : -1 +} +const transferPlnValue = (transfer) => { + return sign(transfer) * Number(transfer.amount) * toPln(transfer.currency) +} +const sum = (xs) => xs.reduce((a, b) => a + b, 0) +function printTransfer(transfer) { + const { title, type, date, amount, currency } = transfer + // type: [ 'IN', 'OUT_TO_OWN', 'BANK_FEE', 'IN_FROM_OWN', 'OUT' ] + const sign = type.startsWith('IN') ? '+' : '-' + console.log( + `${date} | ${pad(15, classify(transfer))} | ${pad(80, title)} | ${sign} ${padRight( + 8, + formatAmmount(amount), + )} ${currency}`, + ) +} +function classify(transfer) { + if ( + transfer.type === 'IN' && + (transfer.to_account === 'PL48195000012006000648890002' || + transfer.title.match(/^\w+ ?- ?(fatty|starving|superfatty) ?- ?sk(l|ł)adka/i) || + transfer.title.match(/^sk(l|ł)adka ?- \w+ ?- ?\d+ ?m/i) || + transfer.title.match(/skladka - wooddy/i)) && + !transfer.title.match(/(kaucj|zwrot|lokata|grant|covid)/i) + ) { + return 'memberships' + } + if ( + transfer.type === 'BANK_FEE' || + transfer.title.match( + /(Opłata za przelew|Miesięczny abonament|ZWROT OPŁATY ZA PROWADZENIE RACHUNKU)/, + ) + ) { + return 'bank_fees' + } + if ( + transfer.type === 'OUT' && + (transfer.to_name.includes('RIPE') || + transfer.to_name.includes('Stowarzyszenie e-Południe') || + transfer.to_name.includes('Nitronet sp. z o. o.') || + transfer.to_name.includes('Nitronet sp. z o.o.')) + ) { + return 'isp_fees' + } + if ( + transfer.to_name === 'PSP Zjednoczenie' || + transfer.title.startsWith('CZYNSZ - GRZYBOWSKA 85c') || + transfer.title.match(/(zwrot kaucji|zwrot wadium|najem lokali)/i) + ) { + return 'rent' + } + if ( + transfer.type === 'OUT' && + (transfer.to_name.startsWith('A&M') || + transfer.title.match(/usługi prawne/i) || + transfer.to_name.match(/Lookreatywni/)) + ) { + return 'legal' + } + if ( + (transfer.type === 'IN' && + transfer.to_account === 'PL64195000012006000648890005' && + transfer.title.toLowerCase().includes('fv')) || + transfer.title.match(/internet BGP.WTF/i) || + transfer.title.match(/internet - umowa HSWAW/i) || + transfer.title.includes('Invoice N. FV/21043') + ) { + return 'bgp_wtf_income' + } + if ( + transfer.type === 'IN' && + transfer.date.startsWith('2020-') && + transfer.title.match( + /(coronavirus|c.vid|przy(ł|l)bic|curvovid|powodzenia m|owner w k|dzialal. promocyj.|^Przelew$|^TRANSFER$|^Outgoing payment$)/i, + ) + ) { + return 'covid19_donation' + } + if ( + transfer.type === 'OUT' && + transfer.date >= '2020-03-31' && + transfer.date <= '2020-06-24' && + transfer.from_account === 'PL91195000012006000648890004' + ) { + return 'covid19' + } + if ( + transfer.type === 'IN' && + transfer.to_account === 'PL64195000012006000648890005' && + transfer.title.match(/(koszulk|\d ?szt)/i) + ) { + return 'swag_sale' + } + if (transfer.type === 'IN' && transfer.title.match(/(grant|mikrodotacja|transza)/i)) { + return 'grant' + } + const projectDonation = transfer.title.match( + /(^\w+[\s-]*(?:darowizna|darownizna|DAROWNIZNA)[\s-]*(.*)$|(?:skladka|darowizna) celowa -? ?(.*)$|skladka na pokrycie (.*)|na zakup (.*)|darowizna na (.*))/i, + ) + if (transfer.type === 'IN' && projectDonation && !transfer.title.match(/cele statutowe/)) { + const cause = projectDonation.slice(2).filter(Boolean)[0] + return `donation_cause` + // return `donation: ${cause.toLowerCase()}` + } + if (transfer.type === 'IN' && transfer.title.match(/(darowizn|uk online giving|testow)/i)) { + return `donation` + } + if ( + transfer.type === 'OUT' && + transfer.title.match( + /(Leroy Merlin Warszawa|Castorama Warszawa|bricoman.pl|zakup materiałów budowlanych|Market Budowlany)/i, + ) + ) { + return 'construction' + } + if (transfer.title.match(/(zwrot z podatku vat|KRS)/i)) { + return 'taxes' + } + if (transfer.from_name.match(/CURRENCY ONE/i) || transfer.to_name.match(/CURRENCY ONE/i)) { + return 'currency_exchange' + } + if (transfer.title.match(/lokata nr/i)) { + return 'time_deposit' + } + if ( + transfer.type === 'OUT' && + (transfer.title.match(/(stal hutnicza|stawex)/) || + transfer.to_name.match(/stawex/i) || + transfer.to_name.match(/nor-gaz/i)) + ) { + return 'materials' + } + if ( + transfer.type === 'OUT' && + transfer.title.match(/(^zwrot |Zakup przy użyciu karty|PayU w Allegro|Przelewy24|^zwroty )/i) + ) { + return 'purchases' + } + if (transfer.type === 'OUT' && transfer.to_name.match(/(eniy soro|radosław pietruszewski)/i)) { + return 'purchases_returns' + } + if ( + transfer.type === 'OUT' && + (transfer.date === '2021-12-21' || transfer.to_name.startsWith('Maker Kids Michał')) + ) { + return 'grant_expenses' + } + if (transfer.type === 'IN') { + return 'in?' + } + if (transfer.type === 'OUT') { + return 'out?' + } + return '??' +} + +const printTransfers = (transfers) => { + transfers.forEach(printTransfer) + console.log('') + + const total = sum(transfers.map(transferPlnValue)) + console.log(`Total: ${Math.round(total) / 100} PLN`) + console.log(`Total/mo: ${Math.round(total / 12 / 100)} PLN`) + console.log(`Transfers: ${transfers.length}`) +} + +const printCategorized = (transfers) => { + const categories = {} + transfers.forEach((tx) => { + const type = classify(tx) + if (!categories[type]) { + categories[type] = [] + } + categories[type].push(tx) + }) + + Object.entries(categories).map(([category, categoryTransfers]) => { + console.log('') + console.log('___________________________') + console.log(`Category: ${category}`) + console.log('') + printTransfers(categoryTransfers) + }) +} + +// printCategorized( +// rawTransfers +// .filter((x) => classify(x) !== 'memberships') +// // .filter((x) => classify(x) === 'bank_fees') +// // .filter((x) => classify(x).startsWith('donation:')) +// // .filter((x) => x.date.startsWith('2021-')) +// // .filter((x) => x.amount > 10_000_00) +// // .filter((x) => x.type === 'OUT_TO_OWN') +// // .filter((x) => classify(x) === '??') +// // .filter((x) => x.title.toLowerCase().includes('uk online giving')) +// // .filter((x) => x.title.match(/kuh/i)) +// // .filter( +// // (x) => +// // x.type === 'IN' && +// // x.date.startsWith('2020-') && +// // x.title.match(/(coronavirus|c.vid|przy(ł|l)bic|curvovid)/i), +// // ) +// // .slice(-500), +// .concat([]), +// ) + +const generateMonthDates = () => { + const now = new Date() + const currentMonth = now.getMonth() + 1 + const currentYear = now.getFullYear() + const dates = [] + for (let y = 2016; y <= currentYear; y++) { + const initialMonth = y == 2016 ? 11 : 1 + const maxMonth = y == currentYear ? currentMonth : 12 + for (let m = initialMonth; m <= maxMonth; m++) { + dates.push(`${y}-${m < 10 ? 0 : ''}${m}`) + } + } + return dates +} +const monthDates = generateMonthDates() + +const printMonthSums = (transfers) => { + const groupped = fp.piped( + transfers, + fp.groupBy((t) => t.date.slice(0, -3)), + ) + monthDates.forEach((date) => { + const txs = groupped[date] || [] + const sum = txs.reduce((sum, transfer) => sum + transferPlnValue(transfer), 0) + console.log(`${date} | ${padRight(8, formatAmmount(Math.round(sum)))}`) + }) +} + +// printMonthSums(rawTransfers.filter((x) => classify(x) === 'bank_fees')) + +const days = rawTransfers + .filter((x) => classify(x) !== 'memberships') + .concat([]) + .map((x) => Number(x.date.match(/.*-(\d{2})$/)[1])) + .sort() + +const dayCounts = Array(32).fill(0) + +days.forEach((day) => { + dayCounts[day] += 1 +}) + +console.log( + dayCounts + .slice(1) + .map((count, day) => ({ day, count })) + .sort((a, b) => b.count - a.count) + .map(({ day, count }) => `${day + 1} | ${count}`) + .join('\n'), +) diff --git a/kasownik_classifier/convert_to_json.js b/kasownik_classifier/convert_to_json.js new file mode 100644 index 0000000..f65aa3b --- /dev/null +++ b/kasownik_classifier/convert_to_json.js @@ -0,0 +1,32 @@ +const fs = require('fs') + +// INSTRUCTIONS FOR GENERATION: +// psql -h 127.0.0.1 -U radex kasownik +// \copy (select * from raw_transfer) to 'raw_transfers.txt' delimiter '~' csv header; +// scp radex@hackerspace.pl:~/raw_transfers.txt . +// node convert_to_json.js + +function zipObj(keys, values) { + const obj = {} + if (keys.length !== values.length) { + console.error(keys) + console.error(values) + throw new Error(`broken row`) + } + keys.forEach((key, i) => { + obj[key] = values[i] + }) + return obj +} + +function convert() { + let data = fs.readFileSync('raw_transfers.txt').toString('utf8').trim().split('\n') + const header = data.shift().split('~') + console.log(header) + const rows = data.filter(Boolean).map((row) => zipObj(header, row.split('~'))) + const json = JSON.stringify(rows, null, ' ') + fs.writeFileSync('raw_transfers.json', json) +} + +convert() +k diff --git a/kasownik_classifier/package.json b/kasownik_classifier/package.json new file mode 100644 index 0000000..c9586eb --- /dev/null +++ b/kasownik_classifier/package.json @@ -0,0 +1,14 @@ +{ + "name": "kasownik_clasifier", + "version": "1.0.0", + "description": "", + "main": "analyze.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "rambdax": "^7.2.0" + } +} diff --git a/kasownik_classifier/prettier.config.js b/kasownik_classifier/prettier.config.js new file mode 100644 index 0000000..afe4f74 --- /dev/null +++ b/kasownik_classifier/prettier.config.js @@ -0,0 +1,7 @@ +module.exports = { + printWidth: 100, + trailingComma: "all", + semi: false, + singleQuote: true, + bracketSpacing: true, +}; diff --git a/kasownik_classifier/yarn.lock b/kasownik_classifier/yarn.lock new file mode 100644 index 0000000..3c1fce5 --- /dev/null +++ b/kasownik_classifier/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +rambdax@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/rambdax/-/rambdax-7.2.0.tgz#f2632cb96a89d330d05fa41b488c12185b46001a" + integrity sha512-l1r7tYf/oRIpCiw1BJB7kSYl9i7H4tJheKbnWq95DM83Q4CazmRYJ+WM/VRaffr09yvMCdgeF4n6Ya4uWBe/Aw==