GithubHelp home page GithubHelp logo

envato / double_entry Goto Github PK

View Code? Open in Web Editor NEW
417.0 93.0 68.0 737 KB

A double-entry accounting system for Ruby applications.

Home Page: https://rubygems.org/gems/double_entry

License: MIT License

Ruby 98.24% Shell 0.14% Logos 1.29% Dockerfile 0.33%
finance ruby gem accounting

double_entry's Introduction

DoubleEntry

License MIT Gem Version Build Status Code Climate

Show me the Money

Keep track of all the monies!

DoubleEntry is an accounting system based on the principles of a Double-entry Bookkeeping system. While this gem acts like a double-entry bookkeeping system, as it creates two entries in the database for each transfer, it does not enforce accounting rules, other than optionally ensuring a balance is positive, and through an allowlist of approved transfers.

DoubleEntry uses the Money gem to encapsulate operations on currency values.

Compatibility

DoubleEntry is tested against:

Ruby

  • 3.3.x
  • 3.2.x
  • 3.1.x
  • 3.0.x

Rails

  • 7.1.x
  • 7.0.x
  • 6.1.x

Databases

  • MySQL
  • PostgreSQL
  • SQLite

Installation

In your application's Gemfile, add:

gem 'double_entry'

Download and install the gem with Bundler:

bundle

Generate Rails schema migrations for the required tables:

The default behavior is to store metadata in a json(b) column rather than a separate double_entry_line_metadata table. If you would like the old (1.x) behavior, you can add --no-json-metadata.

rails generate double_entry:install

Update the local database:

rake db:migrate

Interface

The entire API for recording financial transactions is available through a few methods in the DoubleEntry module. For full details on what the API provides, please view the documentation on these methods.

A configuration file should be used to define a set of accounts, and potential transfers between those accounts. See the Configuration section for more details.

Accounts

Money is kept in Accounts.

Each Account has a scope, which is used to subdivide the account into smaller accounts. For example, an account can be scoped by user to ensure that each user has their own individual account.

Scoping accounts is recommended. Unscoped accounts may perform more slowly than scoped accounts due to lock contention.

To get a particular account:

account = DoubleEntry.account(:spending, scope: user)

(This actually returns an Account::Instance object.)

See DoubleEntry::Account for more info.

Balances

Calling:

account.balance

will return the current balance for an account as a Money object.

Transfers

To transfer money between accounts:

DoubleEntry.transfer(
  Money.new(20_00),
  from: one_account,
  to:   another_account,
  code: :a_business_code_for_this_type_of_transfer,
)

The possible transfers, and their codes, should be defined in the configuration.

See DoubleEntry::Transfer for more info.

Metadata

You may associate arbitrary metadata with transfers, for example:

DoubleEntry.transfer(
  Money.new(20_00),
  from: one_account,
  to:   another_account,
  code: :a_business_code_for_this_type_of_transfer,
  metadata: {key1: ['value 1', 'value 2'], key2: 'value 3'},
)

Locking

If you're doing more than one transfer in a single financial transaction, or you're doing other database operations along with the transfer, you'll need to manually lock the accounts you're using:

DoubleEntry.lock_accounts(account_a, account_b) do
  # Perhaps transfer some money
  DoubleEntry.transfer(Money.new(20_00), from: account_a, to: account_b, code: :purchase)
  # Perform other tasks that should be commited atomically with the transfer of funds...
end

The lock_accounts call generates a database transaction, which must be the outermost transaction.

See DoubleEntry::Locking for more info.

Account Checker/Fixer

DoubleEntry tries really hard to make sure that stored account balances reflect the running balances from the double_entry_lines table, but there is always the unlikely possibility that something will go wrong and the calculated balance might get out of sync with the actual running balance of the lines.

DoubleEntry therefore provides a couple of tools to give you some confidence that things are working as expected.

The DoubleEntry::Validation::LineCheck will check the double_entry_lines table to make sure that the balance column correctly reflects the calculated running balance, and that the double_entry_account_balances table has the correct value in the balance column. If either one of these turn out to be incorrect then it will write an entry into the double_entry_line_checks table reporting on the differences.

You can alternatively pass a fixer to the DoubleEntry::Validation::LineCheck.perform method which will try and correct the balances. This gem provides the DoubleEntry::Validation::AccountFixer class which will correct the balance if it's out of sync.

Using these classes is optional and both are provided for additional safety checks. If you want to make use of them then it's recommended to run them in a scheduled job, somewhere on the order of hourly to daily, depending on transaction volume. Keep in mind that this process locks accounts as it inspects their balances, so it will prevent new transactions from being written for a short time.

Here are examples that could go in your scheduled job, depending on your needs:

# Check all accounts & write the results to the double_entry_line_checks table
DoubleEntry::Validation::LineCheck.perform!

# Check & fix accounts (results will also be written to the table)
DoubleEntry::Validation::LineCheck.perform!(fixer: DoubleEntry::Validation::AccountFixer.new)

See DoubleEntry::Validation for more info.

Implementation

All transfers and balances are stored in the lines table. As this is a double-entry accounting system, each transfer generates two lines table entries: one for the source account, and one for the destination.

Lines table entries also store the running balance for the account. To retrieve the current balance for an account, we find the most recent lines table entry for it.

See DoubleEntry::Line for more info.

AccountBalance records cache the current balance for each Account, and are used to perform database level locking.

Transfer metadata is stored in a json(b) column on both the source and destination lines of the transfer.

Configuration

A configuration file should be used to define a set of accounts, optional scopes on the accounts, and permitted transfers between those accounts.

The configuration file should be kept in your application's load path. For example, config/initializers/double_entry.rb. By default, this file will be created when you run the installer, but you will need to fill out your accounts.

For example, the following specifies two accounts, savings and checking. Each account is scoped by User (where User is an object with an ID), meaning each user can have their own account of each type.

This configuration also specifies that money can be transferred between the two accounts.

require 'double_entry'

DoubleEntry.configure do |config|
  # Use json(b) column in double_entry_lines table to store metadata instead of separate metadata table
  config.json_metadata = true

  config.define_accounts do |accounts|
    user_scope = ->(user) do
      raise 'not a User' unless user.class.name == 'User'
      user.id
    end
    accounts.define(identifier: :savings,  scope_identifier: user_scope, positive_only: true)
    accounts.define(identifier: :checking, scope_identifier: user_scope)
  end

  config.define_transfers do |transfers|
    transfers.define(from: :checking, to: :savings,  code: :deposit)
    transfers.define(from: :savings,  to: :checking, code: :withdraw)
  end
end

By default an account's currency is the same as Money.default_currency from the money gem.

You can also specify a currency on a per account basis. Transfers between accounts of different currencies are not allowed.

DoubleEntry.configure do |config|
  config.define_accounts do |accounts|
    accounts.define(identifier: :savings,  scope_identifier: user_scope, currency: 'AUD')
  end
end

Testing with RSpec

Transfering money needs to be run as a top level transaction. This conflicts with RSpec's default behavior of creating a new transaction for every test, causing an exception of type DoubleEntry::Locking::LockMustBeOutermostTransaction to be raised. This behavior may be disabled by adding the following lines into your rails_helper.rb.

RSpec.configure do |config|
  # ...
  # This first line should already be there. You will need to add the second one
  config.use_transactional_fixtures = true
  DoubleEntry::Locking.configuration.running_inside_transactional_fixtures = true
  # ...
end

Jackhammer

Run a concurrency test on the code.

This spawns a bunch of processes, and does random transactions between a set of accounts, then validates that all the numbers add up at the end.

You can also tell it to flush out the account balances table at regular intervals, to validate that new account balances records get created with the correct balances from the lines table.

./script/jack_hammer -t 20
Cleaning out the database...
Setting up 5 accounts...
Spawning 20 processes...
Flushing balances
Process 1 running 1 transfers...
Process 0 running 1 transfers...
Process 3 running 1 transfers...
Process 2 running 1 transfers...
Process 4 running 1 transfers...
Process 5 running 1 transfers...
Process 6 running 1 transfers...
Process 7 running 1 transfers...
Process 8 running 1 transfers...
Process 9 running 1 transfers...
Process 10 running 1 transfers...
Process 11 running 1 transfers...
Process 12 running 1 transfers...
Process 13 running 1 transfers...
Process 14 running 1 transfers...
Process 16 running 1 transfers...
Process 15 running 1 transfers...
Process 17 running 1 transfers...
Process 19 running 1 transfers...
Process 18 running 1 transfers...
Reconciling...
All the Line records were written, FTW!
All accounts reconciled, FTW!
Done successfully :)

Future Direction

See the Github project issues.

Development Environment Setup

We're using Docker to provide a convenient and consistent environment for executing tests during development. This allows engineers to quickly set up a productive development environment.

Note: Most development files are mounted in the Docker container. This enables engineers to edit files in their favourite editor (on the host OS) and have the changes immediately available in the Docker container to be exercised.

One exception to this is the RSpec configuration. Changes to these files will require a rebuild of the Docker image (step 2).

Prerequisites:

  • Docker
  • Docker Compose
  • Git
  1. Clone this repo.

    git clone [email protected]:envato/double_entry.git && cd double_entry
  2. Build the Docker image we'll use to run tests

    docker-compose build --pull double_entry
  3. Startup a container and attach a terminal. This will also start up a MySQL and Postgres database.

    docker-compose run --rm double_entry ash
  4. Run the tests

    DB=mysql bundle exec rspec
    DB=postgres bundle exec rspec
    DB=sqlite bundle exec rspec
  5. When finished, exit the container terminal and shut down the databases.

    exit
    docker-compose down

Contributors

Many thanks to those who have contributed to both this gem, and the library upon which it was based, over the years:

  • Anthony Sellitti - @asellitt
  • Clinton Forbes - @clinton
  • Eaden McKee - @eadz
  • Giancarlo Salamanca - @salamagd
  • Jiexin Huang - @jiexinhuang
  • Keith Pitt - @keithpitt
  • Kelsey Hannan - @KelseyDH
  • Mark Turnley - @rabidcarrot
  • Martin Jagusch - @MJIO
  • Martin Spickermann - @spickermann
  • Mary-Anne Cosgrove - @macosgrove
  • Orien Madgwick - @orien
  • Pete Yandall - @notahat
  • Rizal Muthi - @rizalmuthi
  • Ryan Allen - @ryan-allen
  • Samuel Cochran - @sj26
  • Stefan Wrobel - @swrobel
  • Stephanie Staub - @stephnacios
  • Trung LĂŞ - @joneslee85
  • Vahid Ta'eed - @vahid

double_entry's People

Contributors

achhetr avatar andyjdavis avatar brewdium avatar dersnek avatar doncote avatar eadz avatar elct9620 avatar fesplugas avatar gja avatar jacobbednarz avatar jiexinhuang avatar kelseydh avatar orien avatar rabidcarrot avatar ricobl avatar rizalmuthi avatar ryanzhou avatar salamagd avatar scottyp-env avatar sj26 avatar spacepotato avatar spickermann avatar stavro avatar stephnacios avatar stevend avatar swrobel avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

double_entry's Issues

Version 2.0 release

Is there anything blocking for version 2.0 release. It has been more than a year for beta version.

Un-scoped accounts are not sortable, fail <=> against scoped accounts.

Just wondering what the suggested way to handle a global account is, i.e. an account that is not scoped to an object. We had started with a global account... accounts.define(identifier: :global) but that fails <=>.

Our idea was:
Payins => Payments => Global => Earnings => Payouts
With global acting as a buffer and also retaining our platform profits.

My understanding is that any double entry transfer has to come from or go to somewhere, so in our setup Payins (credit card transfers) would always be negative and Payouts (to external bank account) would always be positive.

Thanks for the help!

DoubleEntry::TransferNotAllowed between accounts with different scope (but same identifier)

I defined a primary account like so:

config.define_accounts do |accounts|
    user_scope = ->(user) do
      raise 'not a User' unless user.class.name == 'User'
      user.id
    end
    
    accounts.define(identifier: :primary, scope_indentifier: user_scope, ...)
end

And a transfer transfer like so:

config.define_transfers do |transfers|
    transfers.define(from: :primary, to: :primary, code: :transfer)
end

If I try to do a transfer like this:

user1 = User.first
user2 = User.last

account1 = DoubleEntry.account(:primary, scope: user1.id)
account2 = DoubleEntry.account(:primary, scope: user2.id)

DoubleEntry.transfer(Money.new(1),
                     from: account1,
                     to: account2,
                     code: :transfer)

It throws a DoubleEntry::TransferNotAllowed. This strikes me as weird behaviour. They don't have the same scope/user. Users should be able to move money between each other's accounts.

I did some further digging and found that both account1.scope_identity and account2.scope_identity return the same User id.

Can't seem to figure out why.

Remove `DoubleEntry::Line#meta`

The need to store business meta data in each line is dubious. The implementation, marshalling the object to a textual column, brittle. The attribute meta should be removed.

The use case of associating business events to a monetary transfer is satisfied by the polymorphic association of DoubleEntry::Line#detail.

Rails 5 issue: Directly inheriting from ActiveRecord::Migration is not supported. Please specify the Rails release the migration was written for:

StandardError: An error has occurred, this and all later migrations canceled:

Directly inheriting from ActiveRecord::Migration is not supported. Please specify the Rails release the migration was written for:

  class CreateDoubleEntryTables < ActiveRecord::Migration[4.2]
/APP_NAME/db/migrate/20171128034808_create_double_entry_tables.rb:1:in `<top (required)>'
/APP_NAME/config/initializers/task.rb:7:in `execute'

As the error message recommends, I was able to fix this for Rails 5 by specifying the correct version in the migration (currently it just outputs class CreateDoubleEntryTables < ActiveRecord::Migration).

This seems like an easy PR to bring in, and would prevent people from shooting themselves in the foot when adding this gem for the first time.

Positive Only does not work with concurrent calls

I have a ledger running with an account type set to positive_only true. See the Wallet account type in the following:

https://github.com/sudhirj/notecase/blob/master/config/initializers/double_entry.rb

This works fine normally, but on a very small minority of cases where transactions come in at almost the same time, I've seen account balances go negative. Is there anything that can be done to avoid this? Both accounts are being locked as follows:

https://github.com/sudhirj/notecase/blob/master/app/models/transaction.rb

Any ideas on how we can improve the integrity of positive_only accounts?

Use bigint for all IDs

integer is currently being used, and it took our project only a few months to hit more transactions than the 32 bit int supports. I'd suggest changing all IDs to bigint.

Dynamic accounts

Would you recommend using this as a base for an app that will have dynamic accounts? i.e. users creating accounts

DoubleEntry::UnknownAccount: account: spending scoped?: true

On a Rails 5.1.4 app installing double_entry using Postgresql, with the following initializer:

require 'double_entry'

DoubleEntry.configure do |config|
  config.define_accounts do |accounts|
    user_scope = accounts.active_record_scope_identifier(User)
    accounts.define(:identifier => :savings,  :scope_identifier => user_scope, :positive_only => true)
    accounts.define(:identifier => :checking, :scope_identifier => user_scope)
  end

  config.define_transfers do |transfers|
    transfers.define(:from => :checking, :to => :savings,  :code => :deposit)
    transfers.define(:from => :savings,  :to => :checking, :code => :withdraw)
  end
end 

and unchanged migrations (beyond specifying ActiveRecord version 5.1).

I get the following error in console:

=> account = DoubleEntry.account(:spending, :scope => User.first)
  User Load (0.3ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
DoubleEntry::UnknownAccount: account: spending scoped?: true

Is this possibly related to #115? I've tried every combination I can think of for passing in a scope here and it's all failing.

My app already has an Account and Transfer model defined, but surely this shouldn't be of issue given double_entry's module namespacing? Any help much appreciated.

Store `DoubleEntry::Line#scope` as integer

The attribute DoubleEntry::Line#scope is currently stored as a VARCHAR. The use case for this attribute is to link to some business entity. Therefore the column type should be INTEGER; identical to how ActiveRecord IDs are stored.

Scopes don't support UUIDs

Because of the 23 character limit, it looks like it's impossible to support UUIDs as scopes - this is quite odd because they make excellent scopes. Can we do something about this?

Importing Legacy Transactions

Does anyone have a good pattern for importing legacy transactions into DE? This would require recording a transfer at a specific time in the past.

It looks like there are two options:

  1. Modifications to transfer/process/create_lines in order to override default dates

  2. Override Time.now or AR setters for created_at/updated_at in a block outside the transfer creation.

Any ideas, thoughts, gists welcome.

Support UUID Model ids

UUID's are a popular strategy for model id's due to their unique properties across all tables. Unfortunately it appears that this gem does not support uuid's natively, owing to a 17 strong character letter limit on scope objects id.

This can be seen with the following test, which will fail:

    it 'finds account by uuid' do
      object_with_uuid = double(:id => 'fea45df8-2ec0-4e8d-9c5a-5871a5414bca')

      expect do
        DoubleEntry.account(:savings, :scope => object_with_uuid)
      end.to_not raise_error

      expect do
        DoubleEntry.balance(:savings, :scope => object_with_uuid)
      end.to_not raise_error
    end

Which if run will result in this failure showing the problem:

=>      Failure/Error:
       expect do
         DoubleEntry.account(:savings, :scope => object_with_uuid)
       end.to_not raise_error
     
       expected no Exception, got #<DoubleEntry::ScopeIdentifierTooLongError: scope identifier 'fea45df8-2ec0-4e8d-9c5a-5871a5414bca' is too long. Please limit it to 23 characters.> 

with backtrace:
         # ./lib/double_entry/account.rb:167:in `ensure_scope_is_valid'
         # ./lib/double_entry/account.rb:108:in `initialize'
         # ./lib/double_entry/account.rb:27:in `new'
         # ./lib/double_entry/account.rb:27:in `account'
         # ./lib/double_entry.rb:44:in `account'
         # ./spec/double_entry_spec.rb:417:in `block (4 levels) in <top (required)>'
         # ./spec/double_entry_spec.rb:416:in `block (3 levels) in <top (required)>'
     # ./spec/double_entry_spec.rb:416:in `block (3 levels) in <top (required)>'

Instead, double_entry implemention should support both sequential ids and uuid's -- the big benefit with UUID's being we no longer need to worry about the validity of id values and what model its associated with. This makes it easy to use double_entry in outside contexts, like consuming the ledgers of external systems where account objects are represented as api keys or other arbitrary string values.

Migration fails because of duplicate detail fields in double_entry_lines

Steps to reproduce

Install gem with

gem "double_entry", github: "envato/double_entry"

Run

bundle install

Run

rails generate double_entry:install

Run

rails db:migrate

Expected result:
Migration should run successfully, adding required tables.

Actual result:
Migration fails with the following

== 20191106104924 CreateDoubleEntryTables: migrating ==========================
-- create_table("double_entry_account_balances", {:force=>true})
   -> 0.0392s
-- add_index("double_entry_account_balances", ["account"], {:name=>"index_account_balances_on_account"})
   -> 0.0176s
-- add_index("double_entry_account_balances", ["scope", "account"], {:name=>"index_account_balances_on_scope_and_account", :unique=>true})
   -> 0.0203s
-- create_table("double_entry_lines", {:force=>true})
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:

you can't define an already defined column 'detail_id'.
...
.../db/migrate/20191106104924_create_double_entry_tables.rb:23:in `block in up'
...

Caused by:
ArgumentError: you can't define an already defined column 'detail_id'.
...
.../db/migrate/20191106104924_create_double_entry_tables.rb:23:in `block in up'
...

Exactly the same problem happens with detail_type field in the same table.

Reason

On migration from integers to bigints, the following line was added:

t.references "detail", :index => false, :polymorphic => true

However, these lines weren't removed:

t.integer    "detail_id"
t.string     "detail_type"

So detail reference now looks like this in migration:

t.references "detail", :index => false, :polymorphic => true
t.integer    "detail_id"
t.string     "detail_type"

These are duplicate lines and they cause the problem.

Added:
If I remove these two lines, migration runs successfully.

AccountFixer not fix account which scope is nil

Hi, the LineCheck can find the accounts which scope: nil but the AccountFixer didn't fix it correctly.

def lines_for_account(account)
Line.where(
account: account.identifier.to_s,
scope: account.scope_identity.to_s
).order(:id)
end

The line scope: account.scope_identity.to_s always convert nil to "" (empty string) that it unable find correct account lines to update balance.

Possible Options:

  1. Change scope: account.scope_identity.to_s to scope: account.scope_identity&.to_s
  2. Set scope column default to "" instead accept nil

We may consider to the unscoped account in this case if we set all scope to "" instead of nil

Updated

Due to the LineCheck SQL, option 1 will unable to find any Line in this case

previous_line = Line.find_by_sql([<<-SQL, line.account.identifier.to_s, line.scope, line.id])
SELECT * FROM #{Line.quoted_table_name} #{force_index}
WHERE account = ?
AND scope = ?
AND id < ?
ORDER BY id DESC
LIMIT 1
SQL

In this case, the scope = NULL is not working correctly.

My database is PostgreSQL and not sure how MySQL works in this case.

Note

Sometimes one of my accounts which without scope will reset the balance to 0 after the transfer (eg. from 100.0 to 1.0)

But if the LineCheck works correctly, the balance can go back correctly value.

I am not sure the relationship between the above two issues, but it seems we have some problems when using scope if it is nil?

Make detail association optional in DoubleEntry::Line

Do the library enforce the presence of :detail option in DoubleEntry.transfer? If not, please make the detail association in DoubleEntry::Line optional, because Rails 5 makes belongs_to association required by default.

A better way of defining the scope lambda

The documentation includes with example of defining a scope lambda:

    user_scope = lambda do |user_identifier|
      if user_identifier.is_a?(User)
        user_identifier.id
      else
        user_identifier
      end
    end

Let's implement a generic version of this. It should include some sweet syntactic sugar for easy use in Double Entry configuration.

Wrong dates in changelog

I just noticed that the dates in the changelog for releases [2.0.0.beta4] and [2.0.0.beta5] are probably not correct and are actually much more recent according to a "git blame". Just thought you might want to correct that. Many thanks :)

Allow validation on transfer definition

For my project I needed some sort of additional check when I defined transfers.

Something like this:

transfers.define(from: :savings,  to: :checking, code: :withdraw) do |from, to|
  if from.scope.blocked?
    fail DoubleEntry::TransferNotAllowed, 'user is blocked'
  end
end

I monkey patched gem and would like to make a PR if you think it's worth to have.

Here's my monkey patch:

# frozen_string_literal: true

module OverrideDefineMethod
  def define(attributes, &block)
    attributes.merge!(block: block) if block_given?
    super(attributes)
  end

  def find!(from_account, to_account, code)
    transfer = super
    transfer.block.call(from_account, to_account) if transfer.block
    transfer
  end
end

module OverrideTransferInitializer
  attr_reader :block

  def initialize(attributes)
    @block = attributes[:block]
    super(attributes)
  end
end

DoubleEntry::Transfer::Set.prepend OverrideDefineMethod
DoubleEntry::Transfer.prepend OverrideTransferInitializer

DoubleEntry.balance uses Line rather than AccountBalance

This doesn't seem like the most efficient way to go about things, considering there's a table just to store account balances. Am I missing something?

def balance(account, options = {})
  account = account(account, options) if account.is_a? Symbol
  BalanceCalculator.calculate(account, options)
end

should be, imho:

def balance(account, options = {})
  account = account(account, options) if account.is_a? Symbol
  AccountBalance.find_by_account(account).balance
end

Why does #lock_tables need to be the outermost transaction?

Consider the following code:

class Invoice::Payer
  # ...

  def pay
    @invoice.lock do
      payment = CreditCard::Charger.charge(@invoice.account.credit_card, @invoice.description, @invoice.amount)
      if payment.success?
        @invoice.update(paid: true)
      end
    end
  end
end

class CreditCard::Charger
  # ...

  def charge
    # ...

    from_account = DoubleEntry.account(:credit_card, scope: @credit_card.account)
    to_account = DoubleEntry.account(:sales, scope: @credit_card.account)

    DoubleEntry.transfer(@amount, from: from_account, to: to_account, code: :purchase)
  end
end

This seems like pretty reasonable code to me, but unfortunately doesn't work because in the Invoice class, I'm locking the invoice (which creates a transaction), and then the DoubleEntry#transfer method fails because it's transaction is not the outer-most one.

What's the reasoning behind making it the outer-most transaction? What would the world be like if it "just worked"?

Reporting Bug

Im looking into reporting, I think there is a bug in reconciled?:

scoped_lines = Line.where(:account => "#{account.identifier}", :scope => "#{account.scope}")

Should be:

scoped_lines = Line.where(:account => "#{account.identifier}", :scope => "#{account.scope.id}")

Also, to generate a ledger should I just be querying DoubleEntry::Line directly, or is there a method existing for that?

Thanks!

Money.infinite_precision is deprecated

Hey! It looks like money gem changed something inside and log files are full of

[DEPRECATION] Money.infinite_precision is deprecated - use Money.default_infinite_precision instead.

Any plans on fixing it?

DoubleEntry.transfer returns nil

DoubleEntry.transfer always returns nil.

I think it'd be more useful if it returns the credit & debit lines like the create_lines method does.
Is there any reason it returns nil?

AccountScopeMismatchError after an error

Hi!,
I'm getting this error:

DoubleEntry::AccountScopeMismatchError (Expected instance of `User`, received instance of `User`)

every time I made an error in a method using DoubleEntry (even if the error is not DoubleEntry related). This happens in the console and the server, and the only solution is killing the server, restarting the console, and killing Spring.
I'm still using DoubleEntry in development, but I'm very afraid that the issue will cause a lot of trouble in production. Even using this #75 (comment) the issue remains.

Here's a more detailed example:
(everything works fine, on purpose delete a comma in a Hash)

SyntaxError (app/controllers/cartolas_controller.rb:16: syntax error, unexpected tIDENTIFIER, expecting ')'
...eEntry.account(:cilindros scope: x)}}
...                               ^
app/controllers/cartolas_controller.rb:23: syntax error, unexpected keyword_end, expecting '}'
app/controllers/cartolas_controller.rb:57: syntax error, unexpected keyword_end, expecting '}'):
  app/controllers/cartolas_controller.rb:16: syntax error, unexpected tIDENTIFIER, expecting ')'
  app/controllers/cartolas_controller.rb:23: syntax error, unexpected keyword_end, expecting '}'
  app/controllers/cartolas_controller.rb:57: syntax error, unexpected keyword_end, expecting '}'


  Rendered /Users/marcelo/.rvm/gems/ruby-2.2-head@bo_distribuidores/gems/web-console-2.0.0/lib/action_dispatch/templates/rescues/_source.erb (5.4ms)
  Rendered /Users/marcelo/.rvm/gems/ruby-2.2-head@bo_distribuidores/gems/web-console-2.0.0/lib/action_dispatch/templates/rescues/_trace.html.erb (2.5ms)
  Rendered /Users/marcelo/.rvm/gems/ruby-2.2-head@bo_distribuidores/gems/web-console-2.0.0/lib/action_dispatch/templates/rescues/_request_and_response.html.erb (1.3ms)
  Rendered /Users/marcelo/.rvm/gems/ruby-2.2-head@bo_distribuidores/gems/web-console-2.0.0/lib/action_dispatch/templates/rescues/_web_console.html.erb (1.0ms)
  Rendered /Users/marcelo/.rvm/gems/ruby-2.2-head@bo_distribuidores/gems/web-console-2.0.0/lib/action_dispatch/templates/rescues/diagnostics.html.erb within rescues/layout (171.4ms)

Fixing the error, making the same request:

Started GET "/cartolas" for 127.0.0.1 at 2016-05-06 11:27:06 -0400
Processing by CartolasController#index as HTML
[...]
DoubleEntry::AccountScopeMismatchError (Expected instance of `User`, received instance of `User`):
  app/controllers/cartolas_controller.rb:13:in `block in index'
  app/controllers/cartolas_controller.rb:13:in `index'

Only restarting the servers everything is back to normal.

Please help me!!!

Rails 6.1.X Compatibility?

Are there plans to add Rails 6.1.X compatibility? I can raise a PR like #176, assuming there aren't any major problems with Rails 6.1.

Thanks!

Incorrect identifier checking for balance queries

DoubleEntry#balance does not properly check the scope type.

Example configuration:

Assume 2 active record models, User, and Bank

DoubleEntry.configure do |config|
  config.define_accounts do |accounts|
    user_scope = accounts.active_record_scope_identifier(User)
    accounts.define(:identifier => :cash, :scope_identifier => user_scope)

    bank_scope = accounts.active_record_scope_identifier(Bank)
    accounts.define(:identifier => :piggybank, :scope_identifier => bank_scope)
  end

  config.define_transfers do |transfers|
    transfers.define(:from => :cash, :to => :piggybank,  :code => :deposit)
  end
end

Initial transfer:

cash_account = DoubleEntry.account(:cash, scope: User.new(id: 1))
bank_account = DoubleEntry.account(:piggybank, scope: Bank.new(id: 1))

DoubleEntry.transfer(Money.new(100), from: cash_account, to: bank_account, :code => :deposit)

Balance queries:

DoubleEntry.account(:cash, scope: User.new(id: 1)).balance  #=> CORRECT (-100)
DoubleEntry.balance(:cash, scope: User.new(id: 1)) #=> CORRECT (-100)

DoubleEntry.account(:cash, scope: Bank.new(id: 1)).balance #=> CORRECT(0) (Banks should not have :cash accounts)
DoubleEntry.balance(:cash, scope: Bank.new(id: 1)) #=> ***** INCORRECT (-100) *****

Accounting rules

What do you mean by this program does not enforce accounting rules?

Infinite loop for TimeRangeArray#make over daylight savings time

Master branch is currently unable to perform any reporting on a day or hour granularity over a time period that includes daylight savings time.

Example: (set computer time to America/Los_Angeles)

DoubleEntry::Reporting.aggregate_array(
  :sum,
  :checking,
  :save,
  :range_type => 'day',
  :start      => '2014-01-01',
  :finish     => '2014-12-31',
)

This results in an infinite loop within the #make method, due to line 19.

After deeper inspection, the culprit is invalid date calculation in DayRange#from_time.

DoubleEntry::Reporting::DayRange.from_time(Time.parse('2014, Sun Mar 09')).next.to_s
=> "2014, Mon Mar 03"

ActiveRecord deprecation

I got this deprecation warning when executed DoubleEntry::Validation::LineCheck.perform!

DEPRECATION WARNING: Using `return`, `break` or `throw` to exit a transaction block is
deprecated without replacement. If the `throw` came from
`Timeout.timeout(duration)`, pass an exception class as a second
argument so it doesn't use `throw` to abort its block. This results
in the transaction being committed, but in the next release of Rails
it will raise and rollback.
 (called from restartable_transaction at lib/ruby/gems/2.5.0/bundler/gems/double_entry-4ee0d7f9707a/lib/active_record/locking_extensions.rb:10)

I use newest rails

Why do balances become incorrect?

Generally speaking, why do we need to run the account fixer/checker, and how often should we run it?

I just ran it today, and it found probably 20-30 calculation errors made over the past few months. I'm just curious what situations generally cause these balance miscalculations? Thanks for the awesome library btw!

Lack of negative_only account option

We have the positive_only account option, but lack a negative_only option.

For our use-case, a negative_only account option and complementary validation would be quite beneficial.

Need more understand to run LineCheck.perform!

Hi, I still don't know what the function of LineCheck.perform!, I know if the documentation already complete but I still don't get it when and where this function is needed.

So let say I'm a hacker and I run in rails console, and I manipulate the transaction directly to the database and The system will automaticly fix that? or something else?

Please, anyone, tell the simple case to use this function?

# Check all accounts & write the results to the double_entry_line_checks table
DoubleEntry::Validation::LineCheck.perform!

# Check & fix accounts (results will also be written to the table)
DoubleEntry::Validation::LineCheck.perform!(fixer: DoubleEntry::Validation::AccountFixer.new)

For now, I haven't created a background scheduler yet to use that function, I think It isn't complete yet

Support Rails 7

Hi,

When I trying to use it with Rails 7. I get following errors.

rails aborted!
StandardError: Directly inheriting from ActiveRecord::Migration is not supported. Please specify the Active Record release the migration was written for:

  class CreateDoubleEntryTables < ActiveRecord::Migration[7.0]
/Users/zoloo/code/lawmax-web/db/migrate/20220620000008_create_double_entry_tables.rb:1:in `<main>'

Caused by:
StandardError: Directly inheriting from ActiveRecord::Migration is not supported. Please specify the Active Record release the migration was written for:

  class CreateDoubleEntryTables < ActiveRecord::Migration[7.0]
/Users/zoloo/code/lawmax-web/db/migrate/20220620000008_create_double_entry_tables.rb:1:in `<main>'
Tasks: TOP => db:migrate
(See full trace by running task with --trace

It's looks like not support Rails 7.

Can't run tests because of SyntaxError

I've cloned master branch, done everything mentioned in Development Environment Setup, but when I try to run tests with:

bundle exec rake

I get the following error:

Running tests with `DB=mysql`
/home/mark/.rvm/rubies/ruby-2.2.10/bin/ruby -I/home/mark/code/opensource/double_entry/.bundle/ruby/2.2.0/gems/rspec-core-3.9.0/lib:/home/mark/code/opensource/double_entry/.bundle/ruby/2.2.0/gems/rspec-support-3.9.0/lib /home/mark/code/opensource/double_entry/.bundle/ruby/2.2.0/gems/rspec-core-3.9.0/exe/rspec --pattern spec/\*\*\{,/\*/\*\*\}/\*_spec.rb
/home/mark/code/opensource/double_entry/.bundle/ruby/2.2.0/gems/money-6.13.4/lib/money/rates_store/memory.rb:103: warning: private attribute?
/home/mark/code/opensource/double_entry/.bundle/ruby/2.2.0/gems/money-6.13.4/lib/money/rates_store/memory.rb:103: warning: private attribute?
/home/mark/code/opensource/double_entry/.bundle/ruby/2.2.0/gems/money-6.13.4/lib/money/money/formatting_rules.rb:29: warning: private attribute?
/home/mark/code/opensource/double_entry/.bundle/ruby/2.2.0/gems/money-6.13.4/lib/money/money/formatter.rb:250: warning: private attribute?
/home/mark/code/opensource/double_entry/.bundle/ruby/2.2.0/gems/money-6.13.4/lib/money/money/formatter.rb:250: warning: private attribute?
/home/mark/code/opensource/double_entry/.bundle/ruby/2.2.0/gems/money-6.13.4/lib/money/money/formatter.rb:250: warning: private attribute?
/home/mark/code/opensource/double_entry/.bundle/ruby/2.2.0/gems/money-6.13.4/lib/money/money.rb:146: warning: method redefined; discarding old locale_backend=
/home/mark/code/opensource/double_entry/.bundle/ruby/2.2.0/gems/money-6.13.4/lib/money/money.rb:150: warning: method redefined; discarding old use_i18n=
/home/mark/code/opensource/double_entry/lib/double_entry/validation/line_check.rb:97: warning: `<<' after local variable or literal is interpreted as binary operator
/home/mark/code/opensource/double_entry/lib/double_entry/validation/line_check.rb:97: warning: even though it seems like here document

An error occurred while loading spec_support.
Failure/Error: require 'double_entry/validation/line_check'

SyntaxError:
  /home/mark/code/opensource/double_entry/lib/double_entry/validation/line_check.rb:97: syntax error, unexpected <<
            log << <<~MESSAGE unless correct
                     ^
  /home/mark/code/opensource/double_entry/lib/double_entry/validation/line_check.rb:98: syntax error, unexpected **arg, expecting keyword_end
              *********************************
                ^
  /home/mark/code/opensource/double_entry/lib/double_entry/validation/line_check.rb:100: syntax error, unexpected **arg, expecting keyword_end
              *********************************
                ^
  /home/mark/code/opensource/double_entry/lib/double_entry/validation/line_check.rb:102: dynamic constant assignment
# ./.bundle/ruby/2.2.0/gems/activesupport-5.2.3/lib/active_support/dependencies.rb:291:in `require'
# ./.bundle/ruby/2.2.0/gems/activesupport-5.2.3/lib/active_support/dependencies.rb:291:in `block in require'
# ./.bundle/ruby/2.2.0/gems/activesupport-5.2.3/lib/active_support/dependencies.rb:257:in `load_dependency'
# ./.bundle/ruby/2.2.0/gems/activesupport-5.2.3/lib/active_support/dependencies.rb:291:in `require'
# ./lib/double_entry/validation.rb:2:in `<top (required)>'
# ./.bundle/ruby/2.2.0/gems/activesupport-5.2.3/lib/active_support/dependencies.rb:291:in `require'
# ./.bundle/ruby/2.2.0/gems/activesupport-5.2.3/lib/active_support/dependencies.rb:291:in `block in require'
# ./.bundle/ruby/2.2.0/gems/activesupport-5.2.3/lib/active_support/dependencies.rb:257:in `load_dependency'
# ./.bundle/ruby/2.2.0/gems/activesupport-5.2.3/lib/active_support/dependencies.rb:291:in `require'
# ./lib/double_entry.rb:19:in `<top (required)>'
# ./spec/spec_support.rb:1:in `require'
# ./spec/spec_support.rb:1:in `<top (required)>'
No examples found.


Finished in 0.00004 seconds (files took 0.55766 seconds to load)
0 examples, 0 failures, 1 error occurred outside of examples

/home/mark/.rvm/rubies/ruby-2.2.10/bin/ruby -I/home/mark/code/opensource/double_entry/.bundle/ruby/2.2.0/gems/rspec-core-3.9.0/lib:/home/mark/code/opensource/double_entry/.bundle/ruby/2.2.0/gems/rspec-support-3.9.0/lib /home/mark/code/opensource/double_entry/.bundle/ruby/2.2.0/gems/rspec-core-3.9.0/exe/rspec --pattern spec/\*\*\{,/\*/\*\*\}/\*_spec.rb failed

Support Multiple Currencies per Account

Currently, an Account has a configured currency associated to it.
As I understand it, the currency is fixed and enforced for all transfers involving the account.
For our use-case, we'd like to be able to have a single account identifier (ie. :cash, :vendor_payable, etc.) to support multiple currencies.

To clarify, only a single currency would be involved in a transaction.
For example:

vendor = Vendor.take # Some vendor
order  = vendor.orders.payable.first # Some order from the vendor

cash           = DoubleEntry.account(:cash, currency: order.currency)
vendor_payable = DoubleEntry.account(:vendor_payable, currency: order.currency, scope: vendor)

DoubleEntry.transfer(order.vendor_total, from: cash, to: vendor_payable, code: :pay_vendor_for_order)

Essentially, account(:cash, currency: 'CAD') would hold a separate balance from account(:cash, currency: 'USD').

In terms of technical changes required to accomplish this, it looks to be just a matter of adding a currency column to the double_entry_lines table, and scoping queries for Line records by currency (for calculating balances and the like).

While this could be accomplished by defining many different versions of each account (:cash_cad, :cash_usd, :cash_sek, :cash_eur, etc.), it would considerably complicate displaying breakdowns and balances to the internal and end users, and also force all possible transfers to be configured N times (once per currency).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.