Show HN: I've Built an Accounting System

2024-09-1821:0421471github.com

Implementation of a forward-only double-entry accounting method with a relational database. - GitHub - denys-olleik/accounting: Implementation of a forward-only double-entry accounting method wit...

Watch the video

Watch the video on YouTube

Account Type Debit Credit
Assets Increase Decrease
Liabilities Decrease Increase
Equity/Capital Decrease Increase
Revenue/Income Decrease Increase
Expenses Increase Decrease

Watch the video

GAAP and IFRS compliant implementation of a forward-only double-entry accounting method with a relational database. Suitable for U.S. and international organizations.

The only system capable of managing everything from a single laundromat to a cartel, multiple aircraft carriers, and everything in between, uncontaminated by Silicon Valley 🐂💩.

Due to its wide scope, the system is designed to be incomplete, allowing organizations to finalize the implementation.

  • Using: C# • ASP.NET MVC • Vue.js • PostgreSQL

This solution contains example integrations of feature-specific junction tables which group the journal entries into a transaction for specific user action or feature.

A feature should be integrated with the journal if it affects one of the following account types: revenue, expense, liabilities, assets, or equity.

Since every feature that impacts the journal requires a minimum of two entries, a TransactionGuid column is used to group these entries together.

To implement a new feature affecting the journal, create a table to group the transaction. Prefix the table with GeneralLedger..., then append the names of the tables referenced by the foreign keys that appear immediately after the GeneralLedgerId foreign key and before the Reversed... column (excluding the "Id" suffix from each). For example, GeneralLedgerInvoiceInvoiceLine is named as such because the foreign keys after the GeneralLedgerId but before Reversed... columns are InvoiceId and InvoiceLineId. Include a Reversed... column to reference the entries being reversed, and a TransactionGuid for grouping entries. This structure supports a reversal-and-amendment strategy in a forward-only journal, where entries cannot be modified once recorded. For example, if the revenue recognition for invoice line-items needs to be adjusted after initial creation, the original entries are reversed, and new ones are added.

CREATE TABLE "GeneralLedgerInvoiceInvoiceLine"
( "GeneralLedgerInvoiceInvoiceLineID" SERIAL PRIMARY KEY NOT NULL, "GeneralLedgerId" INT NOT NULL, "InvoiceId" INT NOT NULL, "InvoiceLineId" INT NOT NULL, "ReversedGeneralLedgerInvoiceInvoiceLineId" INT NULL, "TransactionGuid" UUID NOT NULL, "Created" TIMESTAMPTZ NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'), "CreatedById" INT NOT NULL, "OrganizationId" INT NOT NULL, FOREIGN KEY ("GeneralLedgerId") REFERENCES "GeneralLedger"("GeneralLedgerID"), FOREIGN KEY ("InvoiceId") REFERENCES "Invoice"("InvoiceID"), FOREIGN KEY ("InvoiceLineId") REFERENCES "InvoiceLine"("InvoiceLineID"), FOREIGN KEY ("ReversedGeneralLedgerInvoiceInvoiceLineId") REFERENCES "GeneralLedgerInvoiceInvoiceLine"("GeneralLedgerInvoiceInvoiceLineID"), FOREIGN KEY ("CreatedById") REFERENCES "User"("UserID"), FOREIGN KEY ("OrganizationId") REFERENCES "Organization"("OrganizationID")
);

This naming strategy makes it easy for developers to understand the relationship between features and the journal.

The GeneralLedgerInvoiceInvoiceLine is used to record entries during creation of the invoice, the modification of the line-items, and the removal of the line-items from the invoice. However, if you require these concepts to be separated even further, you could adopt a different naming strategy, for example, GeneralLedgerInvoiceInvoiceLineCreated, GeneralLedgerInvoiceInvoiceLineUpdated, GeneralLedgerInvoiceInvoiceLineRemoved.

For example, it may be useful to know how much revenue is lost due to line-items being removed from the invoice.

CREATE TABLE "Account"
( "AccountID" SERIAL PRIMARY KEY NOT NULL, "Name" VARCHAR(200) NOT NULL, "Type" VARCHAR(50) NOT NULL CHECK ("Type" IN ('assets', 'liabilities', 'equity', 'revenue', 'expense')), -- ... "Created" TIMESTAMPTZ NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'), "ParentAccountId" INT NULL, "CreatedById" INT NOT NULL, "OrganizationId" INT NOT NULL, FOREIGN KEY ("ParentAccountId") REFERENCES "Account"("AccountID"), FOREIGN KEY ("CreatedById") REFERENCES "User"("UserID"), FOREIGN KEY ("OrganizationId") REFERENCES "Organization"("OrganizationID"), UNIQUE ("Name", "OrganizationId")
);

The ParentAccountId allows for hierarchical relationships which allow for better reporting and analysis. However, ensuring that the ParentAccountId is of the same Type as the AccountID is enforced at application level.

CREATE TABLE "GeneralLedger"
( "GeneralLedgerID" SERIAL PRIMARY KEY NOT NULL, "AccountId" INT NOT NULL, "Credit" DECIMAL(20, 4) NULL, "Debit" DECIMAL(20, 4) NULL, "CurrencyCode" VARCHAR(3) NULL, "ExchangeRate" DECIMAL(12, 5) NULL, "Memo" TEXT NULL, "Created" TIMESTAMPTZ NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'), "CreatedById" INT NOT NULL, "OrganizationId" INT NOT NULL, FOREIGN KEY ("AccountId") REFERENCES "Account"("AccountID"), FOREIGN KEY ("CreatedById") REFERENCES "User"("UserID"), FOREIGN KEY ("OrganizationId") REFERENCES "Organization"("OrganizationID")
);

Journal entries are made in transactions of at least two entries where the sum of credits equals debits.

Double entry is misleading because some people assume the entries of a transactions are made in pairs, or have an even number of entries.

A typical chart-of-accounts might look like...

AccountID Name Type ParentAccountId
1 accounts-receivable assets NULL
2 revenue revenue NULL
3 revenue-service revenue 2
4 revenue-product revenue 2
5 cash assets NULL
6 chase-9988 assets NULL
7 chase-9988-debit-card-2323 assets 6
8 expense expense NULL
9 expense-diesel expense 8
10 expense-meals expense 8
11 expense-maintenance expense 8
12 discover-5555 liabilities NULL
13 inventory assets NULL
14 sales-tax-payable liabilities NULL

Keep an eye on number 14.

Consider a transaction where an invoice is created with one line-item which requires sales tax to be collected...

GeneralLedgerID AccountId Credit Debit
1 1 (ar) 0.00 1100.00
2 2 (revenue) 1000.00 0.00
3 14 (liabilities) 100.00 0.00

The above table shows a valid transaction made up of three entries where debits equal credits.

Examine the TransactionGuid column in the table below from the result of SELECT * FROM "GeneralLedgerInvoiceInvoiceLine"; to identify transaction boundary.

ID GeneralLedgerId InvoiceId InvoiceLineId ReversedId TransactionGuid
1 2 1 1 NULL e0b84e1c-e62d-60f2-0a96-61f895c14fcf
2 1 1 1 NULL e0b84e1c-e62d-60f2-0a96-61f895c14fcf
3 4 1 2 NULL e0b84e1c-e62d-60f2-0a96-61f895c14fcf
4 3 1 2 NULL e0b84e1c-e62d-60f2-0a96-61f895c14fcf
5 5 1 1 1 91977936-5d6a-401b-7ea6-9092eee92918
6 6 1 1 NULL 91977936-5d6a-401b-7ea6-9092eee92918
7 7 1 1 2 91977936-5d6a-401b-7ea6-9092eee92918
8 8 1 1 NULL 91977936-5d6a-401b-7ea6-9092eee92918
9 9 1 2 3 91977936-5d6a-401b-7ea6-9092eee92918
10 10 1 2 NULL 91977936-5d6a-401b-7ea6-9092eee92918
11 11 1 2 4 91977936-5d6a-401b-7ea6-9092eee92918
12 12 1 2 NULL 91977936-5d6a-401b-7ea6-9092eee92918
13 13 1 1 6 227fa74a-3166-01b6-61db-0f6a6af4a52e
14 14 1 1 8 227fa74a-3166-01b6-61db-0f6a6af4a52e

Note the reversing entries and the three separate transactions.

  1. Rows 1-4 invoice with two line-items is created.
  2. Rows 5-12 both line-items updated.
  3. Rows 13-14 one line-item is removed.

To prevent confusion regarding how to determine if a line-item was removed from an invoice, and without adding additional flags or columns to indicate line-item removal, the line-items whose transactions end with a reversal are considered removed from the invoice.

  • Payments are received against invoice line-items.
  • Payments can be partial.
  • The relationship between payment and invoice is many-to-many.

The GeneralLedgerInvoiceInvoiceLinePayment links the journal entries with the payment.

CREATE TABLE "GeneralLedgerInvoiceInvoiceLinePayment"
( "GeneralLedgerInvoiceInvoiceLinePaymentID" SERIAL PRIMARY KEY NOT NULL, "GeneralLedgerId" INT NOT NULL, "InvoiceInvoiceLinePaymentId" INT NOT NULL, "ReversedGeneralLedgerInvoiceInvoiceLinePaymentId" INT NULL, "TransactionGuid" UUID NOT NULL, "Created" TIMESTAMPTZ NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'), "CreatedById" INT NOT NULL, "OrganizationId" INT NOT NULL, FOREIGN KEY ("GeneralLedgerId") REFERENCES "GeneralLedger" ("GeneralLedgerID"), FOREIGN KEY ("InvoiceInvoiceLinePaymentId") REFERENCES "InvoiceInvoiceLinePayment"("InvoiceInvoiceLinePaymentID"), FOREIGN KEY ("ReversedGeneralLedgerInvoiceInvoiceLinePaymentId") REFERENCES "GeneralLedgerInvoiceInvoiceLinePayment" ("GeneralLedgerInvoiceInvoiceLinePaymentID"), FOREIGN KEY ("CreatedById") REFERENCES "User"("UserID"), FOREIGN KEY ("OrganizationId") REFERENCES "Organization" ("OrganizationID")
);

An incoming check is usually entered into the system before it is deposited. Ideally, the check should be entered into a check-in-transit or similar asset account and later synchronized when it appears on the statement.

The pattern repeats in the GeneralLedgerReconciliationTransaction table. A bank or credit card statement contains rows of transactions. Credit card statements will have expense transactions while bank statements will have both expense and revenue transactions.

Each transaction in the statement should be uniquely identifiable, but often the exported CSV does not contain a column to uniquely identify the row.

CREATE TABLE "GeneralLedgerReconciliationTransaction"
( "GeneralLedgerReconciliationTransactionID" SERIAL PRIMARY KEY NOT NULL, "GeneralLedgerId" INT NOT NULL, "ReconciliationTransactionId" INT NOT NULL, "ReversedGeneralLedgerReconciliationTransactionId" INT NULL, "TransactionGuid" UUID NOT NULL, "Created" TIMESTAMPTZ NOT NULL DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'), "CreatedById" INT NOT NULL, "OrganizationId" INT NOT NULL, FOREIGN KEY ("GeneralLedgerId") REFERENCES "GeneralLedger" ("GeneralLedgerID"), FOREIGN KEY ("ReconciliationTransactionId") REFERENCES "ReconciliationTransaction"("ReconciliationTransactionID"), FOREIGN KEY ("ReversedGeneralLedgerReconciliationTransactionId") REFERENCES "GeneralLedgerReconciliationTransaction" ("GeneralLedgerReconciliationTransactionID"), FOREIGN KEY ("CreatedById") REFERENCES "User"("UserID"), FOREIGN KEY ("OrganizationId") REFERENCES "Organization" ("OrganizationID")
);

Integration with banks to pull the transaction data programatically is possible, but CSV import is used as universal fallback option.

  1. Have dotnet installed.
  2. Have psql installed.
    1. Have CREATE EXTENSION IF NOT EXISTS postgis; installed.
    2. Have CREATE EXTENSION IF NOT EXISTS pgcrypto; installed.
  3. Update connection strings in appsettings.json.
  4. Set database-reset.json to true and run. This does two things...
    1. Uses the main database context to create the application database.
    2. Runs create-db... script to create tables, columns, relationships, indexes, etc. I will later separate sample data to be optional.
  5. Use test@example.com pw test to login and choose "Farm To Market LLC", it already has sample chart of accounts.
  6. Create invoice.
  7. Receive payment.

The system will have three levels of backups.

  1. Ledger - System will attempt to perform a backup upon every action/transaction that affects the ledger. Backups will be in human and machine readable format such as csv, json, or xml (you'll get to pick whichever you hate the least).
  2. Database - Performed as often as resources allow, ideally every hour.
  3. Instance - Entire virtual machine instance is backed up daily.

Why doesn't anyone want to be an accountant? Because it's not accounting anymore.

test

I don't remember QwackBooks ever looking like that.

Intuit didn't invent accounting; they've stolen it.

Accounting existed before computers, databases, and software. Transactions were recorded in physical journals using pen and paper, which forced accountants to have a better understanding of company finances due to the process of manual entry, with little room for error. Now, the process has been automated through software, and the journal entries are hidden away, leaving only the final report to spot errors and fraud.

None of you can call yourselves accountants; you're just QwackBooks users. The truck drivers of the office. Your boss thinks of you as a whiny, tail-dragging █████. That's why he needs just one of you who knows the entire system. Maybe you'll get lucky with an assistant, but most likely they'll cause more problems, because it'll be the boss's daughter and she doesn't give a ████.

Companies that adopt my system are far less likely to get audited than those using QwackBooks or some half-baked ERP system thrown together by a committee in India and managed by another in San Francisco. These systems often go through months of customization and setup before they can be used, and still end in failed implementations.


Read the original article

Comments

  • By stevoh 2024-09-1914:583 reply

    It is a good start but the hubris here is quite high (and maybe not a bad thing). Funny readme but rather harsh. Seems I have been trolled enough to provide a response here...

    I am not a fan of "Qwakbooks" and I can't believe I am about to defend it but... it does allow you to enter journal entries manually. In fact, you do not have to use any of the screens overlayed on top of the GL. The additional features are meant to help users do things faster and more efficiently. The details are not hidden at all but most users don't need to see them to be successful.

    QuickBooks has all of the same core functionality including a well structured database too. You can interact with the QuickBooks Online API with a few endpoints and achieve the same thing much faster with scalability depending on which features you would like to use. If you don't believe me, just read through the API documentation which is really easy to follow. https://developer.intuit.com/app/developer/qbo/docs/api/acco...

    So yes, this is a well structured database for an accounting system but beyond that there isn't much here.

    Some other points:

    "Due to its wide scope, the system is designed to be incomplete, allowing organizations to finalize the implementation." The customization involved to make this useable would be quite substantial and unaffordable unless you did it yourself (which would be a distraction from core business)

    "Companies that adopt my system are far less likely to get audited..." This is false. You might survive an audit with a clean general ledger but it doesn't reduce the likelihood. There is also way more to audits than whether the TB balances - the substance of the transactions is obviously more important. ie. the accountant needs to know accounting otherwise audits with any system go poorly.

    At quick glance... both this and QuickBooks do not have purchase order management, capex or prepaid amortization automation, approval workflows, muti-dimension transaction classification (for FP&A), or strong multi-company/currency support. These are all reasons why companies switch to a more robust (and usually more expensive) system. These features are needed for most but not all big companies with hundreds of employees moving around tens of millions of dollars.

    • By elevation 2024-09-2013:333 reply

      > muti-dimension transaction classification

      Is there an SMB package that supports this?

      In a chart of accounts you might have a Travel/meals/lodging account. The IRS will allow you to write off an employee's meal if it meets certain requirements, but as a company, if your policy is to reimburse employees for meals regardless of the write off, you'd need two sub accounts:

      Travel Meals Lodging (IRS Exempt) Travel Meals Lodging (Non IRS Exempt)

      Now let's say I get a government contract where I'm contractually allowed to bill certain items to the program, but the contractual definition doesn't overlap with IRS definition, such that I need four categories to track all my expenses:

      Travel Meals Lodging (Program Exempt, IRS Exempt) Travel Meals Lodging (Program Exempt, Non IRS Exempt) Travel Meals Lodging (Program Non Exempt, IRS Exempt) Travel Meals Lodging (Program Non Exempt, Non IRS Exempt)

      While this technically works, it feels very wrong. I'd love a tool that would let you reconcile the general ledger against more than one Chart of Accounts, or even tag expenses for reporting purposes.

      • By stevoh 2024-09-2014:551 reply

        You are correct in your example and yes, it should feel wrong. That workaround is totally fine though and most small companies will do something like how you have described.

        In your example, using multidimensional accounting you could book one entry to "Travel Meals Lodging" GL account and have the dimensions "Program", "IRS Exempt", "T&E Subcategory". This creates a one-to-many relationship with the transaction instead of having 4+ GLs. You could book to one GL "Travel Meals Lodging" with the dimension values "Program Non Exempt", "Non IRS Exempt" and "Meals". You could design the dimensions differently, but I am just trying to give you an example.

        No, I am not aware of an SMB package that supports this. It probably exists but from what I understand it makes the database more complex (cube?) and everything more compute intensive on the reporting side. Thought I haven't looked into it much - if you find anything, let me know.

        • By elevation 2024-09-2018:471 reply

          I have a couple projects running on GnuCash, but it doesn't support dimensional accounting. Netsuite would work, but anything Oracle is a hard sell in my space.

          Do you recommend any resources for implementing dimensional accounting? Articles? Example schemas?

          • By stevoh 2024-09-2021:04

            If you are coming from GnuCash, Netsuite is probably overkill and/or out-of-budget. Even if you had the resources, I would probably consider Sage first.

            Multidimensional accounting is in effect supplementing the GL with additional data. For each transaction, you could have a database table that adds a column for each new dimension. Or if you don't prefer a relational structure, a json object attached to each transaction. Outside of setting that up, the problem probably becomes the user experience ie. how do you add that information at the same time as posting your entry?

            If you wanted an even more "hacky" way, you could embed the data into the memo/description field or another available field. Unfortunately, the off-the-shelf reporting would not be useable because it would not know how to parse the embedded data.

            Unfortunately, the multidimensional accounting space is dominated by enterprise accounting system offerings. So I don't know of any resources for implementing it outside of switching to those systems.

      • By zie 2024-09-2116:30

        One can do this with budget codes, instead of via accounts. Where you can add essentially unlimited extra information into any transaction. Many western governments use this method.

        An example from the London School of Economics, in PDF: https://info.lse.ac.uk/staff/divisions/Finance-Division/asse...

      • By itsthecourier 2024-09-223:04

        I believe Odoo does this. They tag transactions and even if you have a chart of accounts for your reports, the tags allow these kind of customizations IIRC

    • By journal 2024-09-1915:061 reply

      Intuit has how many? I am one. Give it time.

      • By stevoh 2024-09-1915:07

        That is fair, I can respect that! Keep going!

    • By PopAlongKid 2024-09-2014:231 reply

      >muti-dimension transaction classification

      Could you explain what this means? I've been using Quickbooks for both myself and clients for many years and never heard this term. Quickbooks does support classes (multiple columns on your P&L and Balance Sheet within the same company file), is that what you mean?

      I'm also pretty sure that QB Enterprise supports multiple currencies, purchase orders, and some of the other stuff you mention, but I haven't worked with that version of QB recently. It also has inventory support that is more sophisticated than the basic support in QB Premier.

      • By stevoh 2024-09-2014:45

        "muti-dimension transaction classification (for FP&A)" - The data for internal management/operational financial reporting and budgeting/forecasting/modeling (ie. FP&A) may be quite different than those used for GAAP financial reporting and taxes. The multiple dimensions allows for added "tagging" which can be used to enrich a transaction with more information. An example would be using a dimension of "department" to classify a transaction as belonging to a certain team. Yes, QB has one dimension called "class" but more enterprise systems allow for 10+ or even unlimited. As I mentioned, you only get to needing this once the company is quite large or has more complex financial data needs.

        I don't know exactly what QB Enterprise has but one of the complaints I hear often that that consolidation with multiple entities and multiple currencies is difficult or not possible without additional tools using QB

  • By bbor 2024-09-1821:241 reply

    Thanks for posting, looks pretty polished to me! Lowkey love the design, super modern finance applications do make me irrationally afraid. Perhaps it's trauma from Navient-administered student loans...

    I will say this is by far the most passionate README.md I've ever seen, lol;

      Intuit didn't invent accounting; they've stolen it. Accounting existed before computers, databases, and software. Transactions were recorded in physical journals using pen and paper, which forced accountants to have a better understanding of company finances due to the process of manual entry, with little room for error. Now, the process has been automated through software, and the journal entries are hidden away, leaving only the final report to spot errors and fraud. None of you can call yourselves accountants; you're just QwackBooks users. The truck drivers of the office. Your boss thinks of you as a whiny, tail-dragging *****.
    
    Do you have any particular reason/motivating experience for this ethos? Why not just use a piece of a paper/the default Excel ledger template/the `ledger` CLI tool if it's so important to do things by hand?

    P.S. "Truck drivers of the office"...?

    • By journal 2024-09-1821:351 reply

      There are multiple reasons.

      1. Intuit discontinued QB desktop on may 31 this year. 2. Intuit recently raised prices. 3. QB online has terrible navigation. I can make invoice in my system in less clicks. 4. I needed a feature to optionally print supporting documents with the invoice while having the ability to arrange the order of those documents. I then can integrate with mailing service and have PDF ready to mail programmatically. Instead of having to join PDFs together. (this feature doesn't work cause I can't afford IronPDF, it's the only PDF package I would use). 5. I don't like any limits on users or numbers of invoices I can have in my system. 6. Excel is error prone for more than a few records. Drag and drop accidentally one cell to another? 7. Multi-tenancy. I wanted to manage multiple companies without logging out.

      • By PopAlongKid 2024-09-2014:14

        >1. Intuit discontinued QB desktop on may 31 this year.

        That is false. They have switched to subscription licensing instead of perpetual license. Accountants can still purchase their version of Quickbooks, and yes the price just about doubled in the last few years, to about $1,000 but it's still available.

  • By Havoc 2024-09-1919:291 reply

    >GAAP and IFRS compliant implementation of a forward-only double-entry accounting method

    You may want to replace well most of your lead image. It sounds like gibberish to accountants. It's the accounting equivalent of saying this is an IPv6 compliant x64 CPU. The words are all from the right field but they're put together in a way that makes no sense to someone familiar with it.

    That said accounting software is a fuckin trainwreck on both UI and features so I understand the hn urge to jump on this. Xero basically cornered the young company market by being a web based and being not entirely fuckin terrible.

    • By meiraleal 2024-09-1922:301 reply

      Not knowing what they are doing is the basic premise of disrupting a market. It fails most of the times, but when it succeeds, big chances it was made by someone that didn't really understood the problem

      • By MichaelZuo 2024-09-208:05

        Can you list some examples from the past decade?

HackerNews