accounting

package module
v0.1.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 15, 2025 License: MIT Imports: 3 Imported by: 0

README

Accounting Library for Go

A robust Go library for handling accounting transactions with support for various inventory costing methods and double-entry bookkeeping.

Features

  • Double-Entry Accounting: Enforces double-entry bookkeeping principles ensuring balanced debits and credits
  • Inventory Costing Methods:
    • FIFO (First In, First Out)
    • LIFO (Last In, First Out)
    • WAC (Weighted Average Cost)
    • HIFO (Highest In, First Out)
    • LOFO (Lowest In, First Out)
  • Inventory Management:
    • Track quantities and amounts separately
    • Support for inventory write-downs
    • Proper handling of zero-quantity and zero-amount cases
  • Data Validation:
    • Entry number sequence validation
    • Time sequence validation
    • Balance validation
    • Duplicate account prevention
    • Amount and quantity validation

Installation

go get github.com/HashemJaafar7/accounting

Quick Start

Here's a simple example of recording a purchase and sale using FIFO costing:

package main

import (
	"fmt"
	"time"

	"github.com/HashemJaafar7/accounting"
)

// Set up in-memory storage
var inventoryStore = make(map[accounting.AccountAddress]accounting.Inventory)
var lastEntry accounting.AccountingEntry
var journal []accounting.AccountingEntry

// Set up required helper functions
func getInventory(addr accounting.AccountAddress) (accounting.Inventory, error) {
	return inventoryStore[addr], nil
}

func setInventory(addr accounting.AccountAddress, inv accounting.Inventory) error {
	inventoryStore[addr] = inv
	return nil
}

func getLastEntry() (accounting.AccountingEntry, error) {
	return lastEntry, nil
}

func setEntry(entry accounting.AccountingEntry) error {
	lastEntry = entry
	journal = append(journal, entry)
	return nil
}

func main() {
	const (
		capital   accounting.AccountAddress = -1001
		USD       accounting.AccountAddress = 2001
		inventory accounting.AccountAddress = 1001
		COGS      accounting.AccountAddress = 3001
		revenue   accounting.AccountAddress = -4001
	)
	{
		// Create an entry to start capital
		entry := accounting.AccountingEntry{
			EntryNumber: 1,
			TimeUnix:    time.Now().Unix(),
			DoubleEntry: []accounting.SingleEntry{
				{
					CostFlowType:   accounting.INFLOW, //
					AccountAddress: capital,           // capital
					Quantity:       0,                 //
					Amount:         1000,              // $1000 total
				},
				{
					CostFlowType:   accounting.INFLOW, // Cash going in
					AccountAddress: USD,               // Cash account
					Quantity:       1000,              //
					Amount:         1000,              // $1000 total
				},
			},
		}

		// Add the entry to the journal
		err := accounting.AddToJournal(entry, getInventory, setInventory, getLastEntry, setEntry)
		if err != nil {
			fmt.Printf("Error: %v\n", err)
			return
		}

		// Print the resulting inventory balance
		printInventory()
	}

	{
		purchaseEntry := accounting.AccountingEntry{
			EntryNumber: 2,
			TimeUnix:    time.Now().Unix(),
			DoubleEntry: []accounting.SingleEntry{
				{
					CostFlowType:   accounting.INFLOW,
					AccountAddress: inventory, // Inventory account
					Quantity:       50,        // 50 units
					Amount:         500,       // $500 total
				},
				{
					CostFlowType:   accounting.WAC,
					AccountAddress: USD, // Cash account
					Quantity:       500,
					Amount:         500,
				},
			},
		}

		err := accounting.AddToJournal(purchaseEntry, getInventory, setInventory, getLastEntry, setEntry)
		if err != nil {
			fmt.Printf("Error: %v\n", err)
			return
		}

		// Print the resulting inventory balance
		printInventory()
	}

	{
		// Now sell 60 units at $15 each using FIFO costing
		saleEntry := accounting.AccountingEntry{
			EntryNumber: 3,
			TimeUnix:    time.Now().Unix(),
			DoubleEntry: []accounting.SingleEntry{
				{
					CostFlowType:   accounting.FIFO, // Use FIFO costing
					AccountAddress: inventory,       // Inventory account
					Quantity:       5,               // Sell 5 units
					Amount:         50,              // Cost of goods sold ($10/unit)
				},
				{
					CostFlowType:   accounting.INFLOW, //
					AccountAddress: COGS,              // COGS account
					Quantity:       5,                 //
					Amount:         50,                //
				},
				{
					CostFlowType:   accounting.INFLOW, //
					AccountAddress: USD,               // cash account
					Quantity:       80,                // Sell 60 units
					Amount:         80,                // Cost of goods sold ($10/unit)
				},
				{
					CostFlowType:   accounting.INFLOW, //
					AccountAddress: revenue,           // revenue account
					Quantity:       5,                 // Sell 5 units price 16
					Amount:         80,                //
				},
			},
		}

		err := accounting.AddToJournal(saleEntry, getInventory, setInventory, getLastEntry, setEntry)
		if err != nil {
			fmt.Printf("Error: %v\n", err)
			return
		}

		// Print the resulting inventory balance
		printInventory()
	}

	//output:
	// address: -1001  inventory:[{1 0 1000}]
	// address: 2001   inventory:[{1 1000 1000}]
	// ____________________________________________
	// address: -1001  inventory:[{1 0 1000}]
	// address: 2001   inventory:[{2 500 500}]
	// address: 1001   inventory:[{2 50 500}]
	// ____________________________________________
	// address: 2001   inventory:[{2 500 500} {3 80 80}]
	// address: 1001   inventory:[{2 45 450}]
	// address: 3001   inventory:[{3 5 50}]
	// address: -4001  inventory:[{3 5 80}]
	// address: -1001  inventory:[{1 0 1000}]
	// ____________________________________________
}

func printInventory() {
	for k, v := range inventoryStore {
		fmt.Printf("address: %v\tinventory:%v\n", k, v)
	}
	fmt.Println("____________________________________________")
}

Usage

Account Addresses
  • Positive account addresses are debit-nature accounts
  • Negative account addresses are credit-nature accounts
Cost Flow Types

The library supports multiple cost flow types:

  • INFLOW: For purchases and other additions to inventory
  • WAC: Weighted Average Cost method
  • FIFO: First In, First Out method
  • LIFO: Last In, First Out method
  • HIFO: Highest In, First Out method
  • LOFO: Lowest In, First Out method
  • NONE: For non-inventory transactions
Recording Transactions

Every transaction must:

  1. Have at least two entries (double-entry principle)
  2. Have balanced debits and credits
  3. Have a sequential entry number
  4. Have a timestamp greater than the previous entry
Error Handling

The library provides detailed error messages for common issues:

  • Invalid entry numbers
  • Time sequence violations
  • Unbalanced entries
  • Duplicate accounts in a single entry
  • Insufficient inventory
  • Amount mismatches
  • Invalid cost flow types

Examples

Check the examples_test.go file for comprehensive examples of:

  • Recording inventory purchases
  • Selling inventory using different costing methods
  • Handling inventory write-downs
  • Using weighted average cost method

Best Practices

  1. Always validate the return error from AddToJournal
  2. Use appropriate cost flow types:
    • INFLOW for purchases
    • FIFO/LIFO/WAC for sales
    • NONE if you want to do by hand (Specific Identification Method)
  3. Keep track of your account addresses consistently
  4. Implement proper storage for inventory and journal entries

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

License

MIT

Documentation

Overview

Example (PurchaseInventory)
package main

import (
	"fmt"
	"time"

	"github.com/HashemJaafar7/accounting"
)

// Set up in-memory storage
var inventoryStore = make(map[accounting.AccountAddress]accounting.Inventory)
var lastEntry accounting.AccountingEntry
var journal []accounting.AccountingEntry

// Set up required helper functions
func getInventory(addr accounting.AccountAddress) (accounting.Inventory, error) {
	return inventoryStore[addr], nil
}

func setInventory(addr accounting.AccountAddress, inv accounting.Inventory) error {
	inventoryStore[addr] = inv
	return nil
}

func getLastEntry() (accounting.AccountingEntry, error) {
	return lastEntry, nil
}

func setEntry(entry accounting.AccountingEntry) error {
	lastEntry = entry
	journal = append(journal, entry)
	return nil
}

func main() {
	const (
		capital   accounting.AccountAddress = -1001
		USD       accounting.AccountAddress = 2001
		inventory accounting.AccountAddress = 1001
		COGS      accounting.AccountAddress = 3001
		revenue   accounting.AccountAddress = -4001
	)
	{
		// Create an entry to start capital
		entry := accounting.AccountingEntry{
			EntryNumber: 1,
			TimeUnix:    time.Now().Unix(),
			DoubleEntry: []accounting.SingleEntry{
				{
					CostFlowType:   accounting.INFLOW, //
					AccountAddress: capital,           // capital
					Quantity:       0,                 //
					Amount:         1000,              // $1000 total
				},
				{
					CostFlowType:   accounting.INFLOW, // Cash going in
					AccountAddress: USD,               // Cash account
					Quantity:       1000,              //
					Amount:         1000,              // $1000 total
				},
			},
		}

		// Add the entry to the journal
		err := accounting.AddToJournal(entry, getInventory, setInventory, getLastEntry, setEntry)
		if err != nil {
			fmt.Printf("Error: %v\n", err)
			return
		}

		// Print the resulting inventory balance
		printInventory()
	}

	{
		purchaseEntry := accounting.AccountingEntry{
			EntryNumber: 2,
			TimeUnix:    time.Now().Unix(),
			DoubleEntry: []accounting.SingleEntry{
				{
					CostFlowType:   accounting.INFLOW,
					AccountAddress: inventory, // Inventory account
					Quantity:       50,        // 50 units
					Amount:         500,       // $500 total
				},
				{
					CostFlowType:   accounting.WAC,
					AccountAddress: USD, // Cash account
					Quantity:       500,
					Amount:         500,
				},
			},
		}

		err := accounting.AddToJournal(purchaseEntry, getInventory, setInventory, getLastEntry, setEntry)
		if err != nil {
			fmt.Printf("Error: %v\n", err)
			return
		}

		// Print the resulting inventory balance
		printInventory()
	}

	{
		// Now sell 60 units at $15 each using FIFO costing
		saleEntry := accounting.AccountingEntry{
			EntryNumber: 3,
			TimeUnix:    time.Now().Unix(),
			DoubleEntry: []accounting.SingleEntry{
				{
					CostFlowType:   accounting.FIFO, // Use FIFO costing
					AccountAddress: inventory,       // Inventory account
					Quantity:       5,               // Sell 5 units
					Amount:         50,              // Cost of goods sold ($10/unit)
				},
				{
					CostFlowType:   accounting.INFLOW, //
					AccountAddress: COGS,              // COGS account
					Quantity:       5,                 //
					Amount:         50,                //
				},
				{
					CostFlowType:   accounting.INFLOW, //
					AccountAddress: USD,               // cash account
					Quantity:       80,                // Sell 60 units
					Amount:         80,                // Cost of goods sold ($10/unit)
				},
				{
					CostFlowType:   accounting.INFLOW, //
					AccountAddress: revenue,           // revenue account
					Quantity:       5,                 // Sell 5 units price 16
					Amount:         80,                //
				},
			},
		}

		err := accounting.AddToJournal(saleEntry, getInventory, setInventory, getLastEntry, setEntry)
		if err != nil {
			fmt.Printf("Error: %v\n", err)
			return
		}

		// Print the resulting inventory balance
		printInventory()
	}

	i := accounting.EntryNumber(1)
	iterOnJournalFunction := func() (accounting.AccountingEntry, bool, error) {
		a := journal[i]
		i++
		return a, len(journal) == int(i)+1, nil
	}

	err := accounting.CheckAllTheJournal(setInventory, iterOnJournalFunction)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

}

func printInventory() {
	for k, v := range inventoryStore {
		fmt.Printf("address: %v\tinventory:%v\n", k, v)
	}
	fmt.Println("____________________________________________")
}
Output:
address: -1001  inventory:[{1 0 1000}]
address: 2001   inventory:[{1 1000 1000}]
____________________________________________
address: -1001  inventory:[{1 0 1000}]
address: 2001   inventory:[{2 500 500}]
address: 1001   inventory:[{2 50 500}]
____________________________________________
address: 2001   inventory:[{2 500 500} {3 80 80}]
address: 1001   inventory:[{2 45 450}]
address: 3001   inventory:[{3 5 50}]
address: -4001  inventory:[{3 5 80}]
address: -1001  inventory:[{1 0 1000}]
____________________________________________

Index

Examples

Constants

View Source
const (
	ErrEntryNumberShouldBeBiggerByOne                            = "ErrEntryNumberShouldBeBiggerByOne"
	ErrTimeShouldBeBigger                                        = "ErrTimeShouldBeBigger"
	ErrEntryMustHaveAtLeast_2Entries                             = "ErrEntryMustHaveAtLeast_2Entries"
	ErrDuplicateAccountInEntry                                   = "ErrDuplicateAccountInEntry"
	ErrSumOfAmountsIsNotZeroAndDebitMoreThanCredit               = "ErrSumOfAmountsIsNotZeroAndDebitMoreThanCredit"
	ErrInventoryNotFoundForAccountAddress                        = "ErrInventoryNotFoundForAccountAddress"
	ErrQuantityAndAmountShouldBothBeDebitOrCredit                = "ErrQuantityAndAmountShouldBothBeDebitOrCredit"
	ErrTheCostFlowTypeIsWrong                                    = "ErrTheCostFlowTypeIsWrong"
	ErrInventoryIsEmpty                                          = "ErrInventoryIsEmpty"
	ErrInsufficientQuantityInInventory                           = "ErrInsufficientQuantityInInventory"
	ErrAmountMismatch                                            = "ErrAmountMismatch"
	ErrInsufficientAmountInInventory                             = "ErrInsufficientAmountInInventory"
	ErrTheQuantityAndAmountShouldBeBothPositive                  = "ErrTheQuantityAndAmountShouldBeBothPositive"
	ErrYouShouldUseCostFlowTypeNONEIfYouHaveQuantityOrAmountZero = "ErrYouShouldUseCostFlowTypeNONEIfYouHaveQuantityOrAmountZero"
)

errors

Variables

This section is empty.

Functions

func AddToJournal

func AddToJournal(entry AccountingEntry,
	getInventoryFunction GetInventory,
	setInventoryFunction SetInventory,
	getLastEntryFunction GetLastEntry,
	setEntryFunction SetEntry,
) error

AddToJournal adds a new accounting entry to the journal while maintaining double-entry accounting principles. It takes the following parameters:

  • entry: The AccountingEntry to be added to the journal
  • getInventoryFunction: A function to retrieve the current inventory for an account address
  • setInventoryFunction: A function to update the inventory for an account address
  • getLastEntryFunction: A function to get the last entry from the journal
  • setEntryFunction: A function to save a new entry to the journal

The function performs the following steps: 1. Retrieves current inventory for all accounts involved in the entry 2. Gets the last journal entry for reference 3. Validates and processes the double-entry accounting rules 4. Saves the new entry to the journal 5. Updates the inventory for all affected accounts

Returns an error if any operation fails during the process.

func CheckAllTheJournal

func CheckAllTheJournal(setInventoryFunction SetInventory, iterOnJournalFunction IterOnJournal) error

CheckAllTheJournal iterates through journal entries and processes double-entry accounting by updating account inventories. It takes two function parameters:

setInventoryFunction: A function that updates the inventory for a given address iterOnJournalFunction: A function that iterates through journal entries

The function processes entries sequentially, checking and validating double-entry accounting rules. For each processed entry, it updates an in-memory map of address inventories. Finally, it persists all updated inventories using the setInventoryFunction.

Returns an error if any operation fails during journal processing or inventory updates.

Types

type AccountAddress

type AccountAddress int64

type AccountAddressAndInventory

type AccountAddressAndInventory map[AccountAddress]Inventory

func CheckAndProcessDoubleEntry

func CheckAndProcessDoubleEntry(lastEntryNumber EntryNumber, lastTimeUnix TimeUnix, entry AccountingEntry, accountAddressAndInventoryVariable AccountAddressAndInventory) (AccountAddressAndInventory, error)

CheckAndProcessDoubleEntry validates and processes a double-entry accounting transaction. It ensures the integrity of the accounting entry and updates the inventory records accordingly.

Parameters:

  • lastEntryNumber: The previous entry number for sequence validation
  • lastTimeUnix: The timestamp of the last entry for chronological validation
  • entry: The accounting entry to be processed
  • accountAddressAndInventoryVariable: Current state of inventory records for all accounts

Returns:

  • AccountAddressAndInventory: Updated inventory records after processing the entry
  • error: Error if any validation fails or processing encounters issues

The function performs the following validations:

  • Ensures entry number is sequential
  • Verifies timestamp is after the last entry
  • Checks minimum of 2 entries in double-entry
  • Validates debit and credit balance
  • Prevents duplicate accounts in single entry
  • Verifies positive amounts and quantities
  • Ensures valid cost flow types

After validation, it processes inventory records according to various transaction scenarios:

  • Handles inflow and outflow of quantities and amounts
  • Manages inventory adjustments (zero quantity or amount cases)
  • Applies cost flow accounting methods
  • Removes zero-value inventory records

The function handles different combinations of positive, negative, and zero values for both amounts and quantities, applying appropriate business rules for each case.

type AccountingEntry

type AccountingEntry struct {
	EntryNumber
	TimeUnix // the time in unix in seconds
	DoubleEntry
}

type Amount

type Amount float64

type CostFlowType

type CostFlowType uint8
const (
	INFLOW CostFlowType = iota
	WAC
	FIFO
	LIFO
	HIFO
	LOFO
	NONE
)

type DoubleEntry

type DoubleEntry []SingleEntry

type EntryNumber

type EntryNumber uint64

type GetInventory

type GetInventory func(key AccountAddress) (Inventory, error)

type GetLastEntry

type GetLastEntry func() (AccountingEntry, error)

type Inventory

type Inventory []InventoryRecord

type InventoryRecord

type InventoryRecord struct {
	EntryNumber
	Quantity
	Amount
}

type IsDebit

type IsDebit bool

func GetStatus

func GetStatus(costFlowType CostFlowType, accountAddress AccountAddress) IsDebit

GetStatus determines if a account status a debit based on cost flow type and account address. It compares the cost flow direction (inflow/outflow) with the natural debit/credit state of the account. Returns true if the account status a debit, false if it a credit. Parameters:

  • costFlowType: Indicates whether money/value is flowing in or out (INFLOW/WAC/FIFO/LIFO/HIFO/LOFO/NONE)
  • accountAddress: The address/identifier of the account being affected

func IsNatureDebit

func IsNatureDebit(accountAddress AccountAddress) IsDebit

IsNatureDebit determines if an account has a debit nature based on its address. A positive or zero account address indicates a debit nature account (assets, expenses), while a negative address indicates a credit nature account (liabilities, revenues, equity).

Parameters:

  • accountAddress: The address of the account to check

Returns:

  • isDebit: true if the account has a debit nature, false if credit nature

type IterOnJournal

type IterOnJournal func() (AccountingEntry, bool, error)

type Quantity

type Quantity float64

type SetEntry

type SetEntry func(value AccountingEntry) error

type SetInventory

type SetInventory func(key AccountAddress, value Inventory) error

type SingleEntry

type SingleEntry struct {
	CostFlowType
	AccountAddress
	Quantity
	Amount
}

type TimeUnix

type TimeUnix = int64

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL