kmycli 0.0.1.pre.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cf59742ccf2a3854ff1044a440aff9f2404eacc3
4
+ data.tar.gz: 9e540677e76c08eb6df559326c5fa2ee1c014994
5
+ SHA512:
6
+ metadata.gz: eb66d188a15e63a9fc94052e8534add30bc9a6989a4e57989ecd3a7ee8ac5a57a6647c7e218ca0bff324f777f6fc03e6dd569d4bddc87b81fef92392ace03dad
7
+ data.tar.gz: 2be19ecf02843f39bb60cf08422f4f6cbfb2e33485ee88ca43530a25c176f777567ffe88b1d236a286c489f96b2b89163b9e45da51ac68a8a8490418d3527907
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in kmycli.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Max Hollmann
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # KMyCLI
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'kmycli'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install kmycli
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/kmycli ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'kmycli'
4
+ KMyCLI::CLI::CLI.start
data/kmycli.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'kmycli/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "kmycli"
8
+ spec.version = KMyCLI::VERSION
9
+ spec.authors = ["Max Hollmann"]
10
+ spec.email = ["mail@maxhollmann.de"]
11
+ spec.description = %q{Command line interface for the SQLite3 database of KMyMoney}
12
+ spec.summary = %q{Command line interface for the SQLite3 database of KMyMoney}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = ['kmycli']
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "pry"
25
+ spec.add_development_dependency "awesome_print"
26
+ spec.add_development_dependency "yard"
27
+
28
+ spec.add_runtime_dependency "thor"
29
+ spec.add_runtime_dependency "sqlite3"
30
+ spec.add_runtime_dependency "activerecord"
31
+ spec.add_runtime_dependency "inifile"
32
+ end
data/lib/kmycli.rb ADDED
@@ -0,0 +1,38 @@
1
+ require 'rubygems'
2
+ require 'sqlite3'
3
+ require 'active_record'
4
+ require 'thor'
5
+ require 'inifile'
6
+ require 'ostruct'
7
+ require 'awesome_print' # TODO require only in development
8
+
9
+ require 'kmycli/ext/fraction.rb'
10
+ require 'kmycli/version'
11
+
12
+ module KMyCLI
13
+ LIBRARY_PATH = File.join(File.dirname(__FILE__), 'kmycli')
14
+ MODELS_PATH = File.join(LIBRARY_PATH, 'models')
15
+ CLI_PATH = File.join(LIBRARY_PATH, 'cli')
16
+
17
+ autoload :DB, File.join(LIBRARY_PATH, 'db')
18
+ autoload :Settings, File.join(LIBRARY_PATH, 'settings')
19
+
20
+ module Models
21
+ autoload :Price, File.join(MODELS_PATH, 'price')
22
+ autoload :Transaction, File.join(MODELS_PATH, 'transaction')
23
+ autoload :Split, File.join(MODELS_PATH, 'split')
24
+ autoload :Currency, File.join(MODELS_PATH, 'currency')
25
+ autoload :Account, File.join(MODELS_PATH, 'account')
26
+ autoload :Payee, File.join(MODELS_PATH, 'payee')
27
+ autoload :Institution, File.join(MODELS_PATH, 'institution')
28
+ autoload :KVPair, File.join(MODELS_PATH, 'kv_pair')
29
+ end
30
+
31
+ module CLI
32
+ autoload :CLI, File.join(CLI_PATH, 'cli')
33
+ autoload :Price, File.join(CLI_PATH, 'price')
34
+ autoload :Transaction, File.join(CLI_PATH, 'transaction')
35
+ autoload :Config, File.join(CLI_PATH, 'config')
36
+ end
37
+ end
38
+
@@ -0,0 +1,45 @@
1
+ require 'pry'
2
+
3
+ module KMyCLI
4
+ module CLI
5
+ class CLI < Thor
6
+ attr_accessor :settings, :o
7
+
8
+ method_option "config-file",
9
+ :default => File.expand_path(File.join("~", ".kmycli"))
10
+ method_option "database"
11
+ def initialize(*args)
12
+ super(*args)
13
+
14
+ self.o = OpenStruct.new(options)
15
+
16
+ self.settings = Settings.new(File.expand_path(options['config-file']))
17
+ settings.merge!(options.select { |k, v| %w(
18
+ database
19
+ ).include?(k) })
20
+
21
+ unless DB.connect File.expand_path(settings.database)
22
+ raise Thor::Error.new("Could not open database '#{settings.database}'")
23
+ end
24
+
25
+ settings.default_currency = Models::KVPair.get('kmm-baseCurrency')
26
+ end
27
+
28
+ desc 'console', 'Open a console'
29
+ map 'c' => 'console'
30
+ def console
31
+ binding.pry
32
+ end
33
+
34
+ desc 'version', 'Display installed KMyCLI version'
35
+ map '-v' => :version
36
+ def version
37
+ puts "KMyCLI #{VERSION}"
38
+ end
39
+
40
+ register Price, :price, "price", "Price commands"
41
+ register Transaction, :transaction, "transaction", "Transaction commands"
42
+ register Config, :config, "config", "Configuration commands"
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,28 @@
1
+ require 'pry'
2
+
3
+ module KMyCLI
4
+ module CLI
5
+ class Config < Thor
6
+ attr_accessor :settings, :o
7
+
8
+ def initialize(*args)
9
+ super(*args)
10
+ self.o = OpenStruct.new(options)
11
+ self.settings = Settings.new(File.expand_path(o['config-file']))
12
+ end
13
+
14
+ desc 'list', 'List configuration variables'
15
+ def list
16
+ key_l = settings.all.keys.map(&:length).max
17
+ settings.all.each do |k, v|
18
+ say "#{k.ljust(key_l)} = #{v}"
19
+ end
20
+ end
21
+
22
+ desc 'set [key] [value]', 'Set a configuration variable'
23
+ def set(k, v)
24
+ settings.set k, v
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,116 @@
1
+ require 'pry'
2
+
3
+ module KMyCLI
4
+ module CLI
5
+ class Price < Thor
6
+ attr_accessor :o
7
+
8
+ def initialize(*args)
9
+ super(*args)
10
+ self.o = OpenStruct.new(options)
11
+ end
12
+
13
+ desc 'list', 'List prices'
14
+ method_option 'from',
15
+ :aliases => "-f"
16
+ method_option 'to',
17
+ :aliases => "-t"
18
+ method_option 'date',
19
+ :aliases => "-d"
20
+ def list
21
+ prices = Models::Price.all
22
+ prices = prices.from_currency(o.from) if o.from.present?
23
+ prices = prices.to_currency(Models::Currency.find(o.to)) unless o.to.nil?
24
+ prices = prices.where(:priceDate => o.date) if o.date.present?
25
+ list_prices prices.order(:priceDate => :asc)
26
+ end
27
+
28
+ desc 'add', 'Add conversion rate for a currency to another currency'
29
+ method_option 'from',
30
+ :aliases => "-f"
31
+ method_option 'to',
32
+ :aliases => "-t"
33
+ method_option 'price',
34
+ :aliases => "-p"
35
+ method_option 'date',
36
+ :aliases => "-d"
37
+ method_option 'no-inverse',
38
+ :aliases => "-n",
39
+ :type => :boolean
40
+ method_option 'force',
41
+ :type => :boolean
42
+ def add
43
+ o.from ||= ask("From currency:")
44
+ o.to ||= ask("To currency:")
45
+ o.price ||= ask("Price:")
46
+ o.date ||= ask("Date (default: #{Date.today.iso8601})")
47
+ o.date = Date.today.iso8601 unless o.date.present?
48
+
49
+ date = Date.iso8601(o.date)
50
+ prices = [
51
+ new_price(o.from, o.to, o.price, date),
52
+ new_price(o.to, o.from, 1.0 / o.price.to_f, date)
53
+ ]
54
+ list_prices prices
55
+ if o.force || ask("Create the above prices? [Yn]").downcase == 'y'
56
+ prices.each do |p|
57
+ say ""
58
+ if p.save
59
+ say "Price was added successfully:", Thor::Shell::Color::GREEN
60
+ list_prices [p]
61
+ else
62
+ say "Couldn't save price:", Thor::Shell::Color::RED
63
+ list_prices [p]
64
+ say p.errors.full_messages.join("\n")
65
+ end
66
+ end
67
+ else
68
+ say "Cancelled.", Thor::Shell::Color::RED
69
+ end
70
+ end
71
+
72
+ desc 'delete', 'Delete conversion rates matching the options'
73
+ method_option 'from',
74
+ :aliases => "-f"
75
+ method_option 'to',
76
+ :aliases => "-t"
77
+ method_option 'price',
78
+ :aliases => "-p"
79
+ method_option 'date',
80
+ :aliases => "-d"
81
+ method_option 'force',
82
+ :type => :boolean
83
+ def delete
84
+ prices = Models::Price.all
85
+ prices = prices.where(:fromId => o.from) if o.from.present?
86
+ prices = prices.where(:toId => o.to) if o.to.present?
87
+ prices = prices.where(:priceDate => o.date) if o.date.present?
88
+
89
+ list_prices prices
90
+ if o.force || ask("Delete the above prices? Type 'yes' to confirm (or run with the --force option)").downcase == 'yes'
91
+ n = prices.delete_all
92
+ puts "Deleted #{n} prices.", Thor::Shell::Color::GREEN
93
+ else
94
+ say "Cancelled.", Thor::Shell::Color::RED
95
+ end
96
+ end
97
+
98
+ no_commands do
99
+ def list_prices(prices)
100
+ prices.each do |p|
101
+ say "#{p.priceDate}\t#{p.fromId} -> #{p.toId}\t#{p.price.eval_fraction}"
102
+ end
103
+ end
104
+
105
+ def new_price(from, to, price, date)
106
+ Models::Price.new({
107
+ :from => Models::Currency.find(from),
108
+ :to => Models::Currency.find(to),
109
+ :price => price.to_f.to_fraction_s,
110
+ :priceDate => date
111
+ })
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,121 @@
1
+ module KMyCLI
2
+ module CLI
3
+ class Transaction < Thor
4
+ attr_accessor :o
5
+
6
+ def initialize(*args)
7
+ super(*args)
8
+ self.o = OpenStruct.new(options)
9
+ end
10
+
11
+ desc 'add ACCOUNT PAYEE CATEGORY AMOUNT [CATEGORY AMOUNT...]', 'Add a transaction'
12
+ method_option :date,
13
+ :aliases => '-d'
14
+ method_option :memo,
15
+ :aliases => '-m'
16
+ def add(account, payee, *splits)
17
+ t = build_transaction(o.date || Date.today, account, payee, splits, o.memo)
18
+ say t.to_s
19
+ if ask("Save the above transaction? [Yn]") =~ /^y/i
20
+ if t.save
21
+ say "Transaction saved.", Thor::Shell::Color::GREEN
22
+ else
23
+ say "Transaction could not be saved.", Thor::Shell::Color::RED
24
+ end
25
+ else
26
+ say "Cancelled.", Thor::Shell::Color::RED
27
+ end
28
+ end
29
+
30
+ desc 'import [FILE] [FILE...]', 'Import transactions from files'
31
+ method_option 'force',
32
+ :type => :boolean
33
+ def import(*files)
34
+ transactions = []
35
+ skipped = []
36
+
37
+ read_stream = Proc.new do |stream|
38
+ date = Date.today
39
+ stream.each do |line|
40
+ line.chomp!
41
+ # TODO be more lenient with date formats
42
+ if line =~ /^\d{4}-\d{2}-\d{2}/
43
+ date = DateTime.iso8601(line)
44
+ elsif line =~ /^\d{2}-\d{2}/
45
+ date = DateTime.iso8601("#{Date.today.year}-#{line}")
46
+ elsif line =~ /^\d{2}/
47
+ date = DateTime.iso8601("#{Date.today.year}-#{Date.today.month.to_s.rjust(2, '0')}-#{line}")
48
+ else
49
+ args = Shellwords.shellwords(line)
50
+ if args.size >= 4
51
+ begin
52
+ t = build_transaction(date, args[0], args[1], args[2..-1])
53
+ transactions << t
54
+ raise "Invalid transaction" unless t.valid?
55
+ rescue => e
56
+ skipped << [line, e.message]
57
+ end
58
+ else
59
+ skipped << [line, "Not enough arguments"] if args.size > 0
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ if files.nil? || files.empty?
66
+ read_stream.call(STDIN)
67
+ else
68
+ files.each do |file|
69
+ File.open(file, 'r', &read_stream)
70
+ end
71
+ end
72
+
73
+ if skipped.any?
74
+ say "Skipped (#{skipped.size}):", Thor::Shell::Color::RED
75
+ say "#{skipped.map { |l, m| "\"#{l}\" (#{m})" }.join("\n")}\n\n"
76
+ end
77
+
78
+ if transactions.any?
79
+ say transactions.map(&:to_s).join("\n\n") + "\n"*2
80
+ if o.force || ask("Save the above transactions? [Yn]") =~ /^y/i
81
+ saved = 0
82
+ transactions.each do |t|
83
+ if t.save
84
+ saved += 1
85
+ else
86
+ puts "Invalid:\n#{t.to_s}\n"
87
+ end
88
+ end
89
+ say "Saved #{saved} transactions", Thor::Shell::Color::GREEN
90
+ else
91
+ say "Cancelled.", Thor::Shell::Color::RED
92
+ end
93
+ else
94
+ say "No transactions found"
95
+ end
96
+ end
97
+
98
+
99
+ no_commands do
100
+ def build_transaction(date, account, payee, splits, memo = nil)
101
+ raise "No splits given" if splits.none?
102
+ raise "AMOUNT for the last split missing" if splits.size.odd?
103
+
104
+ a = Models::Account.accounts.search_one!(account)
105
+ p = Models::Payee.search_one!(payee)
106
+
107
+ t = Models::Transaction.new(:currencyId => 'EUR') # TODO use settings.default_currency
108
+ t.build_main_split a, p, date, o.memo
109
+ splits.each_slice(2) do |category, amount|
110
+ amount = amount.to_f
111
+ c = amount > 0 ? Models::Account.expense_categories.search_one!(category)
112
+ : Models::Account.income_categories.search_one!(category)
113
+ t.add_split c, amount
114
+ end
115
+ t.prepare_for_save
116
+ t
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
data/lib/kmycli/db.rb ADDED
@@ -0,0 +1,13 @@
1
+ module KMyCLI
2
+ class DB
3
+ def self.connect(file)
4
+ return false unless File.exist?(file)
5
+ ActiveRecord::Base.establish_connection(
6
+ :adapter => 'sqlite3',
7
+ :database => file
8
+ )
9
+ ActiveRecord::Base.connection
10
+ ActiveRecord::Base.connected?
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ class Float
2
+ def to_fraction_s
3
+ "#{numerator}/#{denominator}"
4
+ end
5
+ end
6
+
7
+ class String
8
+ def fraction?
9
+ !!match(/^-?\d+(\/)-?\d+$/)
10
+ end
11
+
12
+ def eval_fraction
13
+ a, op, b = scan(/(-?\d+)(\/)(-?\d+)/)[0]
14
+ raise ArgumentError, "'#{self}' is not a valid fraction" unless a.present? && op == "/" && b.present? && b.to_i != 0
15
+ a.to_f / b.to_f
16
+ end
17
+ end
@@ -0,0 +1,52 @@
1
+ module KMyCLI
2
+ module Models
3
+ class Account < ActiveRecord::Base
4
+ self.table_name = "kmmAccounts"
5
+
6
+ has_many :children, :class_name => "Account", :foreign_key => "parentId"
7
+ has_many :splits, :foreign_key => "accountId"
8
+ belongs_to :institution, :foreign_key => "institutionId"
9
+ belongs_to :parent, :class_name => "Account", :foreign_key => "parentId"
10
+ belongs_to :currency, :foreign_key => "currencyId"
11
+
12
+ def path
13
+ parent.present? ? "#{parent.path}:#{accountName}" : accountName
14
+ end
15
+
16
+ def income_category?
17
+ accountType == '12'
18
+ end
19
+ def expense_category?
20
+ accountType == '13'
21
+ end
22
+
23
+ def self.categories
24
+ where(:accountType => %w(12 13))
25
+ end
26
+ def self.income_categories
27
+ where(:accountType => '12')
28
+ end
29
+ def self.expense_categories
30
+ where(:accountType => '13')
31
+ end
32
+ def self.accounts
33
+ where(:accountType => %w(1 2 3 4 9)) # TODO complete list of account types
34
+ end
35
+
36
+ def self.search(query)
37
+ where("accountName LIKE ?", "%#{query.gsub('*', '%')}%")
38
+ end
39
+ def self.search!(query)
40
+ r = search(query)
41
+ raise "Could not find any account matching '#{query}'" if r.none?
42
+ r
43
+ end
44
+ def self.search_one(query)
45
+ search(query).first
46
+ end
47
+ def self.search_one!(query)
48
+ search!(query).first
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,14 @@
1
+ module KMyCLI
2
+ module Models
3
+ class Currency < ActiveRecord::Base
4
+ self.table_name = "kmmCurrencies"
5
+ self.primary_key = "ISOcode"
6
+ self.inheritance_column = nil
7
+
8
+ has_many :prices_to, :class_name => "Price", :foreign_key => "toId"
9
+ has_many :prices_from, :class_name => "Price", :foreign_key => "fromId"
10
+ has_many :transactions, :foreign_key => "currencyId"
11
+ has_many :accounts, :foreign_key => "currencyId"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ module KMyCLI
2
+ module Models
3
+ class Institution < ActiveRecord::Base
4
+ self.table_name = "kmmInstitutions"
5
+
6
+ has_many :transactions, :foreign_key => "bankId"
7
+ has_many :accounts, :foreign_key => "institutionId"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,16 @@
1
+ module KMyCLI
2
+ module Models
3
+ class KVPair < ActiveRecord::Base
4
+ self.table_name = "kmmKeyValuePairs"
5
+ self.primary_key = "kvpKey"
6
+
7
+ def self.get(key)
8
+ find(key).kvpData
9
+ end
10
+
11
+ def readonly?
12
+ true
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ module KMyCLI
2
+ module Models
3
+ class Payee < ActiveRecord::Base
4
+ self.table_name = "kmmPayees"
5
+
6
+ has_many :splits, :foreign_key => "payeeId"
7
+
8
+ def self.search(query)
9
+ where("name LIKE ?", "%#{query.gsub('*', '%')}%")
10
+ end
11
+ def self.search!(query)
12
+ r = search(query)
13
+ raise "Could not find any payee matching '#{query}'" if r.none?
14
+ r
15
+ end
16
+ def self.search_one(query)
17
+ search(query).first
18
+ end
19
+ def self.search_one!(query)
20
+ search!(query).first
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ module KMyCLI
2
+ module Models
3
+ class Price < ActiveRecord::Base
4
+ self.table_name = "kmmPrices"
5
+
6
+ belongs_to :from, :class_name => "Currency", :foreign_key => "fromId"
7
+ belongs_to :to, :class_name => "Currency", :foreign_key => "toId"
8
+
9
+ validates :priceDate, :uniqueness => { :scope => [:fromId, :toId] }
10
+
11
+ default_scope { order(:priceDate => :asc).where("LENGTH(fromId) = 3") }
12
+
13
+ def self.to_currency(code)
14
+ where(:toId => code)
15
+ end
16
+ def self.from_currency(code)
17
+ where(:fromId => code)
18
+ end
19
+
20
+ before_save do
21
+ self.price = price.to_f.to_fraction_s unless price.is_a?(String) && price.fraction?
22
+ self.priceFormatted = price.eval_fraction.round(2).to_s
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ module KMyCLI
2
+ module Models
3
+ class Split < ActiveRecord::Base
4
+ self.table_name = "kmmSplits"
5
+
6
+ belongs_to :transaction, :foreign_key => "transactionId"
7
+ belongs_to :payee, :foreign_key => "payeeId"
8
+ belongs_to :account, :foreign_key => "accountId"
9
+
10
+ before_save do
11
+ self.txType ||= 'N'
12
+ self.postDate ||= Date.today
13
+ self.reconcileFlag ||= "0"
14
+
15
+ self.value = shares if value.nil? && shares.present?
16
+ self.shares = value if shares.nil? && value.present?
17
+ self.value = value.to_f.to_fraction_s unless value.is_a?(String) && value.fraction?
18
+ self.valueFormatted = value.eval_fraction.round(2).to_s
19
+ self.shares = shares.to_f.to_fraction_s unless shares.is_a?(String) && shares.fraction?
20
+ self.sharesFormatted = shares.eval_fraction.round(2).to_s
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,77 @@
1
+ module KMyCLI
2
+ module Models
3
+ class Transaction < ActiveRecord::Base
4
+ self.table_name = "kmmTransactions"
5
+
6
+ has_many :splits, :foreign_key => "transactionId", :dependent => :destroy
7
+ belongs_to :institution, :foreign_key => "bankId"
8
+ belongs_to :currency, :foreign_key => "currencyId"
9
+
10
+ validates :currencyId, :presence => true
11
+ validates :entryDate, :presence => true
12
+ validates :postDate, :presence => true
13
+
14
+ # Needs to be called before add_split
15
+ def build_main_split(account, payee, date = Date.today, memo = nil)
16
+ raise "Transaction has splits already. Call this before adding other splits" if splits.any?
17
+ splits.build(
18
+ :splitId => 0,
19
+ :payeeId => payee.id,
20
+ :memo => memo,
21
+ :postDate => date,
22
+ :accountId => account.id
23
+ )
24
+ end
25
+
26
+ def add_split(category, amount, memo = nil)
27
+ raise "build_main_split needs to be called before adding other splits" if splits.empty?
28
+ ms = splits.first
29
+ splits.build(
30
+ :splitId => splits.size,
31
+ :payeeId => ms.payeeId,
32
+ :value => amount,
33
+ :memo => memo,
34
+ :postDate => ms.postDate,
35
+ :accountId => category.id
36
+ )
37
+ end
38
+
39
+ def prepare_for_save
40
+ ms = splits.first
41
+ ms.value = - splits[1..-1].map { |s| s.value.is_a?(String) && s.value.fraction? ? s.value.eval_fraction : s.value.to_f }.sum
42
+
43
+ self.entryDate ||= Date.today
44
+ self.postDate ||= ms.postDate || Date.today
45
+ self.txType ||= 'N'
46
+ end
47
+
48
+ before_save do
49
+ prepare_for_save
50
+ if id.blank?
51
+ self.id = Transaction.next_free_id
52
+ end
53
+ end
54
+
55
+ def self.next_free_id
56
+ highest = Transaction.order(:id => :desc).first.id[1..-1]
57
+ 'T' + (highest.to_i + 1).to_s.rjust(highest.length, '0')
58
+ end
59
+
60
+ def to_s
61
+ ms = splits.first
62
+ o = [
63
+ "Date: #{postDate}",
64
+ "Payee: #{ms.payee.name}",
65
+ "Account: #{ms.account.path}",
66
+ "Total: #{ms.value} #{currencyId}"
67
+ ]
68
+ splits[1..-1].each do |s|
69
+ o << " #{"#{-s.value} #{currencyId}".ljust 20} #{s.account.path}"
70
+ end
71
+ o << "Memo: #{ms.memo}" if ms.memo.present?
72
+ o.join("\n")
73
+ end
74
+
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,33 @@
1
+ module KMyCLI
2
+ class Settings
3
+ attr_accessor :inifile, :settings
4
+
5
+ def initialize(file)
6
+ self.inifile = IniFile.load(file)
7
+ self.settings = {
8
+ }.merge(inifile['global'])
9
+ end
10
+
11
+ def merge!(options)
12
+ self.settings.merge!(options.reject { |k, v| v.nil? })
13
+ end
14
+
15
+ def all
16
+ settings
17
+ end
18
+
19
+ def set(key, val)
20
+ inifile['global'][key] = val
21
+ inifile.write
22
+ end
23
+
24
+ def method_missing(key, *args)
25
+ key = key.to_s
26
+ if key.end_with? "="
27
+ settings[key.chomp("=")] = args[0]
28
+ else
29
+ settings[key]
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module KMyCLI
2
+ VERSION = "0.0.1-alpha.1"
3
+ end
metadata ADDED
@@ -0,0 +1,210 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kmycli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.pre.alpha.1
5
+ platform: ruby
6
+ authors:
7
+ - Max Hollmann
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-10-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: awesome_print
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: yard
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: thor
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: sqlite3
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: activerecord
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '>='
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: inifile
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - '>='
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - '>='
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: Command line interface for the SQLite3 database of KMyMoney
154
+ email:
155
+ - mail@maxhollmann.de
156
+ executables:
157
+ - kmycli
158
+ extensions: []
159
+ extra_rdoc_files: []
160
+ files:
161
+ - .gitignore
162
+ - Gemfile
163
+ - LICENSE.txt
164
+ - README.md
165
+ - Rakefile
166
+ - bin/kmycli
167
+ - kmycli.gemspec
168
+ - lib/kmycli.rb
169
+ - lib/kmycli/cli/cli.rb
170
+ - lib/kmycli/cli/config.rb
171
+ - lib/kmycli/cli/price.rb
172
+ - lib/kmycli/cli/transaction.rb
173
+ - lib/kmycli/db.rb
174
+ - lib/kmycli/ext/fraction.rb
175
+ - lib/kmycli/models/account.rb
176
+ - lib/kmycli/models/currency.rb
177
+ - lib/kmycli/models/institution.rb
178
+ - lib/kmycli/models/kv_pair.rb
179
+ - lib/kmycli/models/payee.rb
180
+ - lib/kmycli/models/price.rb
181
+ - lib/kmycli/models/split.rb
182
+ - lib/kmycli/models/transaction.rb
183
+ - lib/kmycli/settings.rb
184
+ - lib/kmycli/version.rb
185
+ homepage: ''
186
+ licenses:
187
+ - MIT
188
+ metadata: {}
189
+ post_install_message:
190
+ rdoc_options: []
191
+ require_paths:
192
+ - lib
193
+ required_ruby_version: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - '>='
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ required_rubygems_version: !ruby/object:Gem::Requirement
199
+ requirements:
200
+ - - '>'
201
+ - !ruby/object:Gem::Version
202
+ version: 1.3.1
203
+ requirements: []
204
+ rubyforge_project:
205
+ rubygems_version: 2.1.5
206
+ signing_key:
207
+ specification_version: 4
208
+ summary: Command line interface for the SQLite3 database of KMyMoney
209
+ test_files: []
210
+ has_rdoc: