double_entry 0.10.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rubocop.yml +5 -1
- data/README.md +16 -0
- data/double_entry.gemspec +35 -0
- data/lib/double_entry.rb +1 -0
- data/lib/double_entry/line.rb +1 -0
- data/lib/double_entry/line_metadata.rb +18 -0
- data/lib/double_entry/locking.rb +11 -0
- data/lib/double_entry/reporting.rb +26 -14
- data/lib/double_entry/reporting/aggregate.rb +11 -15
- data/lib/double_entry/reporting/aggregate_array.rb +1 -1
- data/lib/double_entry/reporting/line_aggregate.rb +2 -23
- data/lib/double_entry/reporting/line_aggregate_filter.rb +81 -0
- data/lib/double_entry/transfer.rb +42 -25
- data/lib/double_entry/version.rb +1 -1
- data/lib/generators/double_entry/install/templates/migration.rb +10 -0
- data/spec/double_entry/locking_spec.rb +29 -0
- data/spec/double_entry/performance/double_entry_performance_spec.rb +32 -0
- data/spec/double_entry/performance/reporting/aggregate_performance_spec.rb +50 -0
- data/spec/double_entry/reporting/aggregate_array_spec.rb +10 -10
- data/spec/double_entry/reporting/aggregate_spec.rb +57 -114
- data/spec/double_entry/reporting/line_aggregate_filter_spec.rb +90 -0
- data/spec/double_entry/reporting/line_aggregate_spec.rb +35 -7
- data/spec/double_entry/reporting_spec.rb +136 -0
- data/spec/double_entry/transfer_spec.rb +58 -1
- data/spec/support/performance_helper.rb +26 -0
- data/spec/support/schema.rb +9 -0
- metadata +124 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2de3c5bcd2513815b3240072a87abed07c054176
|
4
|
+
data.tar.gz: 441671942df5faff21b7db426075a8dd26822fbc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 88e504ea0d5504c1ce05e63a01ad5697b1cc045298c51a02b52b8bfafb969e5facf0235e727ba600f83b317483b72c9ce284d3dd6e5ac1227aefb40d339696f9
|
7
|
+
data.tar.gz: a669b5f44cc5e955f9c942301748bac1622f58e34082f892b9c12f38dd76aa8d16a85533d8e8e08ab0d2a6440498eba81b93c4cc2311a67538cc8fd5607cd3ae
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -42,6 +42,9 @@ Style/ModuleFunction:
|
|
42
42
|
Style/ParallelAssignment:
|
43
43
|
Enabled: false
|
44
44
|
|
45
|
+
Style/SingleLineBlockParams:
|
46
|
+
Enabled: false
|
47
|
+
|
45
48
|
Metrics/AbcSize:
|
46
49
|
Max: 47 #15
|
47
50
|
|
@@ -55,7 +58,8 @@ Metrics/MethodLength:
|
|
55
58
|
Max: 30 # 10
|
56
59
|
|
57
60
|
Metrics/ModuleLength:
|
58
|
-
|
61
|
+
Exclude:
|
62
|
+
- spec/**/*
|
59
63
|
|
60
64
|
Metrics/PerceivedComplexity:
|
61
65
|
Max: 13 #7
|
data/README.md
CHANGED
@@ -124,6 +124,19 @@ The possible transfers, and their codes, should be defined in the configuration.
|
|
124
124
|
|
125
125
|
See **DoubleEntry::Transfer** for more info.
|
126
126
|
|
127
|
+
### Metadata
|
128
|
+
|
129
|
+
You may associate arbitrary metadata with transfers, for example:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
DoubleEntry.transfer(
|
133
|
+
Money.new(20_00),
|
134
|
+
:from => one_account,
|
135
|
+
:to => another_account,
|
136
|
+
:code => :a_business_code_for_this_type_of_transfer,
|
137
|
+
:metadata => {:key1 => 'value 1', :key2 => 'value 2'},
|
138
|
+
)
|
139
|
+
```
|
127
140
|
|
128
141
|
### Locking
|
129
142
|
|
@@ -160,6 +173,9 @@ See **DoubleEntry::Line** for more info.
|
|
160
173
|
AccountBalance records cache the current balance for each Account, and are used
|
161
174
|
to perform database level locking.
|
162
175
|
|
176
|
+
Transfer metadata is stored as key/value pairs associated with both the source and destination lines of the transfer.
|
177
|
+
See **DoubleEntry::LineMetadata** for more info.
|
178
|
+
|
163
179
|
## Configuration
|
164
180
|
|
165
181
|
A configuration file should be used to define a set of accounts, optional scopes on
|
data/double_entry.gemspec
CHANGED
@@ -17,6 +17,34 @@ Gem::Specification.new do |gem|
|
|
17
17
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
18
|
gem.require_paths = ['lib']
|
19
19
|
|
20
|
+
gem.post_install_message = <<-'POSTINSTALLMESSAGE'
|
21
|
+
Please note the following changes in DoubleEntry:
|
22
|
+
- New table `double_entry_line_metadata` has been introduced and is *required* for
|
23
|
+
aggregate reporting filtering to work. Existing applications must manually manage
|
24
|
+
this change via a migration similar to the following:
|
25
|
+
|
26
|
+
class CreateDoubleEntryLineMetadata < ActiveRecord::Migration
|
27
|
+
def self.up
|
28
|
+
create_table "#{DoubleEntry.table_name_prefix}line_metadata", :force => true do |t|
|
29
|
+
t.integer "line_id", :null => false
|
30
|
+
t.string "key", :limit => 48, :null => false
|
31
|
+
t.string "value", :limit => 64, :null => false
|
32
|
+
t.timestamps :null => false
|
33
|
+
end
|
34
|
+
|
35
|
+
add_index "#{DoubleEntry.table_name_prefix}line_metadata",
|
36
|
+
["line_id", "key", "value"],
|
37
|
+
:name => "lines_meta_line_id_key_value_idx"
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.down
|
41
|
+
drop_table "#{DoubleEntry.table_name_prefix}line_metadata"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
Please ensure that you update your database accordingly.
|
46
|
+
POSTINSTALLMESSAGE
|
47
|
+
|
20
48
|
gem.add_dependency 'money', '>= 6.0.0'
|
21
49
|
gem.add_dependency 'activerecord', '>= 3.2.0'
|
22
50
|
gem.add_dependency 'activesupport', '>= 3.2.0'
|
@@ -35,4 +63,11 @@ Gem::Specification.new do |gem|
|
|
35
63
|
gem.add_development_dependency 'machinist'
|
36
64
|
gem.add_development_dependency 'timecop'
|
37
65
|
gem.add_development_dependency 'rubocop', '~> 0.32.0'
|
66
|
+
|
67
|
+
gem.add_development_dependency 'pry'
|
68
|
+
gem.add_development_dependency 'pry-doc'
|
69
|
+
gem.add_development_dependency 'pry-byebug' if RUBY_VERSION >= '2.0.0'
|
70
|
+
gem.add_development_dependency 'pry-stack_explorer'
|
71
|
+
gem.add_development_dependency 'awesome_print'
|
72
|
+
gem.add_development_dependency 'ruby-prof'
|
38
73
|
end
|
data/lib/double_entry.rb
CHANGED
data/lib/double_entry/line.rb
CHANGED
@@ -0,0 +1,18 @@
|
|
1
|
+
module DoubleEntry
|
2
|
+
class LineMetadata < ActiveRecord::Base
|
3
|
+
class SymbolWrapper
|
4
|
+
def self.load(string)
|
5
|
+
return unless string
|
6
|
+
string.to_sym
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.dump(symbol)
|
10
|
+
return unless symbol
|
11
|
+
symbol.to_s
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
belongs_to :line
|
16
|
+
serialize :key, SymbolWrapper
|
17
|
+
end
|
18
|
+
end
|
data/lib/double_entry/locking.rb
CHANGED
@@ -41,6 +41,13 @@ module DoubleEntry
|
|
41
41
|
else
|
42
42
|
lock.perform_lock(&Proc.new)
|
43
43
|
end
|
44
|
+
|
45
|
+
rescue ActiveRecord::StatementInvalid => exception
|
46
|
+
if exception.message =~ /lock wait timeout/i
|
47
|
+
raise LockWaitTimeout
|
48
|
+
else
|
49
|
+
raise
|
50
|
+
end
|
44
51
|
end
|
45
52
|
|
46
53
|
# Return the account balance record for the given account name if there's a
|
@@ -177,5 +184,9 @@ module DoubleEntry
|
|
177
184
|
# Raised if things go horribly, horribly wrong. This should never happen.
|
178
185
|
class LockDisaster < RuntimeError
|
179
186
|
end
|
187
|
+
|
188
|
+
# Raised if waiting for locks times out.
|
189
|
+
class LockWaitTimeout < RuntimeError
|
190
|
+
end
|
180
191
|
end
|
181
192
|
end
|
@@ -8,6 +8,7 @@ require 'double_entry/reporting/week_range'
|
|
8
8
|
require 'double_entry/reporting/month_range'
|
9
9
|
require 'double_entry/reporting/year_range'
|
10
10
|
require 'double_entry/reporting/line_aggregate'
|
11
|
+
require 'double_entry/reporting/line_aggregate_filter'
|
11
12
|
require 'double_entry/reporting/time_range_array'
|
12
13
|
|
13
14
|
module DoubleEntry
|
@@ -32,19 +33,27 @@ module DoubleEntry
|
|
32
33
|
# The transfers included in the calculation can be limited by time range
|
33
34
|
# and provided custom filters.
|
34
35
|
#
|
35
|
-
# @example Find the sum for all $10 :save transfers in all :checking accounts in the current month (assume the date is January 30, 2014).
|
36
|
+
# @example Find the sum for all $10 :save transfers in all :checking accounts in the current month, made by Australian users (assume the date is January 30, 2014).
|
36
37
|
# time_range = DoubleEntry::Reporting::TimeRange.make(2014, 1)
|
37
38
|
#
|
38
39
|
# DoubleEntry::Line.class_eval do
|
39
|
-
# scope :
|
40
|
+
# scope :specific_transfer_amount, ->(amount) { where(:amount => amount.fractional) }
|
40
41
|
# end
|
41
42
|
#
|
42
43
|
# DoubleEntry::Reporting.aggregate(
|
43
44
|
# :sum,
|
44
45
|
# :checking,
|
45
46
|
# :save,
|
46
|
-
#
|
47
|
-
# :filter => [
|
47
|
+
# time_range,
|
48
|
+
# :filter => [
|
49
|
+
# :scope => {
|
50
|
+
# :name => :specific_transfer_amount,
|
51
|
+
# :arguments => [Money.new(10_00)]
|
52
|
+
# },
|
53
|
+
# :metadata => {
|
54
|
+
# :user_location => 'AU'
|
55
|
+
# },
|
56
|
+
# ]
|
48
57
|
# )
|
49
58
|
# @param [Symbol] function The function to perform on the set of transfers.
|
50
59
|
# Valid functions are :sum, :count, and :average
|
@@ -53,21 +62,24 @@ module DoubleEntry
|
|
53
62
|
# @param [Symbol] code The application specific code for the type of
|
54
63
|
# transfer to perform an aggregate calculation on. As specified in the
|
55
64
|
# transfer configuration.
|
56
|
-
# @
|
57
|
-
#
|
58
|
-
# @option options :filter [Array<Symbol
|
59
|
-
#
|
60
|
-
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
65
|
+
# @param [DoubleEntry::Reporting::TimeRange] Only include transfers in the
|
66
|
+
# given time range in the calculation.
|
67
|
+
# @option options :filter [Array<Hash<Symbol,Hash<Symbol,Object>>>]
|
68
|
+
# An array of custom filter to apply before performing the aggregate
|
69
|
+
# calculation. Filters can be either scope filters, where the name must be
|
70
|
+
# specified, or they can be metadata filters, where the key/value pair to
|
71
|
+
# match on must be specified.
|
72
|
+
# Scope filters must be monkey patched as scopes into the DoubleEntry::Line
|
73
|
+
# class, as the example above shows. Scope filters may also take a list of
|
74
|
+
# arguments to pass into the monkey patched scope, and, if provided, must
|
75
|
+
# be contained within an array.
|
64
76
|
# @return [Money, Fixnum] Returns a Money object for :sum and :average
|
65
77
|
# calculations, or a Fixnum for :count calculations.
|
66
78
|
# @raise [Reporting::AggregateFunctionNotSupported] The provided function
|
67
79
|
# is not supported.
|
68
80
|
#
|
69
|
-
def aggregate(function, account, code, options = {})
|
70
|
-
Aggregate.
|
81
|
+
def aggregate(function, account, code, range, options = {})
|
82
|
+
Aggregate.formatted_amount(function, account, code, range, options)
|
71
83
|
end
|
72
84
|
|
73
85
|
# Perform an aggregate calculation on a set of transfers for an account
|
@@ -2,16 +2,19 @@
|
|
2
2
|
module DoubleEntry
|
3
3
|
module Reporting
|
4
4
|
class Aggregate
|
5
|
-
attr_reader :function, :account, :code, :range, :
|
5
|
+
attr_reader :function, :account, :code, :range, :filter, :currency
|
6
6
|
|
7
|
-
def
|
7
|
+
def self.formatted_amount(function, account, code, range, options = {})
|
8
|
+
new(function, account, code, range, options).formatted_amount
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(function, account, code, range, options = {})
|
8
12
|
@function = function.to_s
|
9
13
|
fail AggregateFunctionNotSupported unless %w(sum count average).include?(@function)
|
10
14
|
|
11
15
|
@account = account
|
12
16
|
@code = code ? code.to_s : nil
|
13
|
-
@
|
14
|
-
@range = options[:range]
|
17
|
+
@range = range
|
15
18
|
@filter = options[:filter]
|
16
19
|
@currency = DoubleEntry::Account.currency(account)
|
17
20
|
end
|
@@ -69,15 +72,8 @@ module DoubleEntry
|
|
69
72
|
if function == 'average'
|
70
73
|
calculate_yearly_average
|
71
74
|
else
|
72
|
-
|
73
|
-
|
74
|
-
total + Reporting.aggregate(
|
75
|
-
function,
|
76
|
-
account,
|
77
|
-
code,
|
78
|
-
:range => MonthRange.new(:year => range.year, :month => month),
|
79
|
-
:filter => filter,
|
80
|
-
)
|
75
|
+
result = (1..12).inject(formatted_amount(0)) do |total, month|
|
76
|
+
total + Aggregate.new(function, account, code, MonthRange.new(:year => range.year, :month => month), :filter => filter).formatted_amount
|
81
77
|
end
|
82
78
|
result.is_a?(Money) ? result.cents : result
|
83
79
|
end
|
@@ -86,8 +82,8 @@ module DoubleEntry
|
|
86
82
|
def calculate_yearly_average
|
87
83
|
# need this seperate function, because an average of averages is not the correct average
|
88
84
|
year_range = YearRange.new(:year => range.year)
|
89
|
-
sum =
|
90
|
-
count =
|
85
|
+
sum = Aggregate.new(:sum, account, code, year_range, :filter => filter).formatted_amount
|
86
|
+
count = Aggregate.new(:count, account, code, year_range, :filter => filter).formatted_amount
|
91
87
|
(count == 0) ? 0 : (sum / count).cents
|
92
88
|
end
|
93
89
|
|
@@ -39,7 +39,7 @@ module DoubleEntry
|
|
39
39
|
# (this includes aggregates for the still-running period)
|
40
40
|
all_periods.each do |period|
|
41
41
|
unless @aggregates[period.key]
|
42
|
-
@aggregates[period.key] =
|
42
|
+
@aggregates[period.key] = Aggregate.formatted_amount(function, account, code, period, :filter => filter)
|
43
43
|
end
|
44
44
|
end
|
45
45
|
end
|
@@ -3,32 +3,11 @@ module DoubleEntry
|
|
3
3
|
module Reporting
|
4
4
|
class LineAggregate < ActiveRecord::Base
|
5
5
|
def self.aggregate(function, account, code, range, named_scopes)
|
6
|
-
|
7
|
-
collection =
|
8
|
-
collection = collection.where(:created_at => range.start..range.finish)
|
9
|
-
collection = collection.where(:code => code) if code
|
6
|
+
collection_filter = LineAggregateFilter.new(account, code, range, named_scopes)
|
7
|
+
collection = collection_filter.filter
|
10
8
|
collection.send(function, :amount)
|
11
9
|
end
|
12
10
|
|
13
|
-
# a lot of the trickier reports will use filters defined
|
14
|
-
# in named_scopes to bring in data from other tables.
|
15
|
-
def self.aggregate_collection(named_scopes)
|
16
|
-
if named_scopes
|
17
|
-
collection = DoubleEntry::Line
|
18
|
-
named_scopes.each do |named_scope|
|
19
|
-
if named_scope.is_a?(Hash)
|
20
|
-
method_name = named_scope.keys[0]
|
21
|
-
collection = collection.send(method_name, named_scope[method_name])
|
22
|
-
else
|
23
|
-
collection = collection.send(named_scope)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
collection
|
27
|
-
else
|
28
|
-
DoubleEntry::Line
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
11
|
def key
|
33
12
|
"#{year}:#{month}:#{week}:#{day}:#{hour}"
|
34
13
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module DoubleEntry
|
3
|
+
module Reporting
|
4
|
+
class LineAggregateFilter
|
5
|
+
def initialize(account, code, range, filter_criteria)
|
6
|
+
@account = account
|
7
|
+
@code = code
|
8
|
+
@range = range
|
9
|
+
@filter_criteria = filter_criteria || []
|
10
|
+
end
|
11
|
+
|
12
|
+
def filter
|
13
|
+
@collection ||= apply_filters
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :account, :code, :range, :filter_criteria
|
19
|
+
|
20
|
+
def apply_filters
|
21
|
+
collection = apply_filter_criteria.
|
22
|
+
where(:account => account).
|
23
|
+
where(:created_at => range.start..range.finish)
|
24
|
+
collection = collection.where(:code => code) if code
|
25
|
+
|
26
|
+
collection
|
27
|
+
end
|
28
|
+
|
29
|
+
# a lot of the trickier reports will use filters defined
|
30
|
+
# in filter_criteria to bring in data from other tables.
|
31
|
+
# For example:
|
32
|
+
#
|
33
|
+
# filter_criteria = [
|
34
|
+
# # an example of calling a named scope called with arguments
|
35
|
+
# {
|
36
|
+
# :scope => {
|
37
|
+
# :name => :ten_dollar_purchases_by_category,
|
38
|
+
# :arguments => [:cat_videos, :cat_pictures]
|
39
|
+
# }
|
40
|
+
# },
|
41
|
+
# # an example of calling a named scope with no arguments
|
42
|
+
# {
|
43
|
+
# :scope => {
|
44
|
+
# :name => :ten_dollar_purchases
|
45
|
+
# }
|
46
|
+
# },
|
47
|
+
# # an example of providing a single metadatum criteria to filter on
|
48
|
+
# {
|
49
|
+
# :metadata => {
|
50
|
+
# :meme => :business_cat
|
51
|
+
# }
|
52
|
+
# }
|
53
|
+
# ]
|
54
|
+
def apply_filter_criteria
|
55
|
+
filter_criteria.reduce(DoubleEntry::Line) do |collection, filter|
|
56
|
+
if filter[:scope].present?
|
57
|
+
filter_by_scope(collection, filter[:scope])
|
58
|
+
elsif filter[:metadata].present?
|
59
|
+
filter_by_metadata(collection, filter[:metadata])
|
60
|
+
else
|
61
|
+
collection
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def filter_by_scope(collection, scope)
|
67
|
+
collection.public_send(scope[:name], *scope[:arguments])
|
68
|
+
end
|
69
|
+
|
70
|
+
def filter_by_metadata(collection, metadata)
|
71
|
+
metadata.reduce(collection.joins(:metadata)) do |filtered_collection, (key, value)|
|
72
|
+
filtered_collection.where(metadata_table => { :key => key, :value => value })
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def metadata_table
|
77
|
+
DoubleEntry::LineMetadata.table_name.to_sym
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -17,11 +17,10 @@ module DoubleEntry
|
|
17
17
|
# @api private
|
18
18
|
def transfer(amount, options = {})
|
19
19
|
fail TransferIsNegative if amount < Money.zero
|
20
|
-
|
21
|
-
|
20
|
+
from_account = options[:from]
|
21
|
+
to_account = options[:to]
|
22
22
|
code = options[:code]
|
23
|
-
|
24
|
-
transfers.find!(from, to, code).process(amount, from, to, code, detail)
|
23
|
+
transfers.find!(from_account, to_account, code).process(amount, options)
|
25
24
|
end
|
26
25
|
end
|
27
26
|
|
@@ -72,34 +71,52 @@ module DoubleEntry
|
|
72
71
|
end
|
73
72
|
end
|
74
73
|
|
75
|
-
def process(amount,
|
76
|
-
|
77
|
-
|
74
|
+
def process(amount, options)
|
75
|
+
from_account = options[:from]
|
76
|
+
to_account = options[:to]
|
77
|
+
code = options[:code]
|
78
|
+
detail = options[:detail]
|
79
|
+
metadata = options[:metadata]
|
80
|
+
if from_account.scope_identity == to_account.scope_identity && from_account.identifier == to_account.identifier
|
81
|
+
fail TransferNotAllowed, 'from account and to account are identical'
|
78
82
|
end
|
79
|
-
if
|
80
|
-
fail MismatchedCurrencies, "
|
83
|
+
if to_account.currency != from_account.currency
|
84
|
+
fail MismatchedCurrencies, "Mismatched currency (#{to_account.currency} <> #{from_account.currency})"
|
81
85
|
end
|
82
|
-
Locking.lock_accounts(
|
83
|
-
credit, debit =
|
86
|
+
Locking.lock_accounts(from_account, to_account) do
|
87
|
+
credit, debit = create_lines(amount, code, detail, from_account, to_account)
|
88
|
+
create_line_metadata(credit, debit, metadata) if metadata
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def create_lines(amount, code, detail, from_account, to_account)
|
93
|
+
credit, debit = Line.new, Line.new
|
84
94
|
|
85
|
-
|
86
|
-
|
95
|
+
credit_balance = Locking.balance_for_locked_account(from_account)
|
96
|
+
debit_balance = Locking.balance_for_locked_account(to_account)
|
87
97
|
|
88
|
-
|
89
|
-
|
98
|
+
credit_balance.update_attribute :balance, credit_balance.balance - amount
|
99
|
+
debit_balance.update_attribute :balance, debit_balance.balance + amount
|
90
100
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
101
|
+
credit.amount, debit.amount = -amount, amount
|
102
|
+
credit.account, debit.account = from_account, to_account
|
103
|
+
credit.code, debit.code = code, code
|
104
|
+
credit.detail, debit.detail = detail, detail
|
105
|
+
credit.balance, debit.balance = credit_balance.balance, debit_balance.balance
|
96
106
|
|
97
|
-
|
107
|
+
credit.partner_account, debit.partner_account = to_account, from_account
|
108
|
+
|
109
|
+
credit.save!
|
110
|
+
debit.partner_id = credit.id
|
111
|
+
debit.save!
|
112
|
+
credit.update_attribute :partner_id, debit.id
|
113
|
+
[credit, debit]
|
114
|
+
end
|
98
115
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
116
|
+
def create_line_metadata(credit, debit, metadata)
|
117
|
+
metadata.each_pair do |key, value|
|
118
|
+
LineMetadata.create!(:line => credit, :key => key, :value => value)
|
119
|
+
LineMetadata.create!(:line => debit, :key => key, :value => value)
|
103
120
|
end
|
104
121
|
end
|
105
122
|
end
|