double_entry 0.10.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|