aspire_budget 0.0.1 → 0.0.2
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/README.md +86 -5
- data/lib/aspire_budget.rb +13 -0
- data/lib/aspire_budget/configuration.rb +50 -0
- data/lib/aspire_budget/core_extensions.rb +122 -0
- data/lib/aspire_budget/models/category_transfer.rb +38 -0
- data/lib/{models → aspire_budget/models}/transaction.rb +6 -7
- data/lib/aspire_budget/utils.rb +62 -0
- data/lib/aspire_budget/version.rb +7 -0
- data/lib/aspire_budget/worksheets/backend_data.rb +17 -0
- data/lib/aspire_budget/worksheets/category_transfers.rb +25 -0
- data/lib/aspire_budget/worksheets/transactions.rb +72 -0
- data/lib/aspire_budget/worksheets/worksheet_base.rb +58 -0
- metadata +177 -25
- data/.gitignore +0 -11
- data/.rspec +0 -3
- data/.rubocop.yml +0 -11
- data/.travis.yml +0 -6
- data/Gemfile +0 -11
- data/Gemfile.lock +0 -109
- data/Rakefile +0 -8
- data/aspire_budget.gemspec +0 -30
- data/bin/console +0 -18
- data/bin/setup +0 -8
- data/lib/client.rb +0 -44
- data/lib/utils.rb +0 -43
- data/lib/version.rb +0 -5
- data/lib/worksheets/backend_data.rb +0 -27
- data/lib/worksheets/transactions.rb +0 -53
- data/lib/worksheets/worksheet_base.rb +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cffb2693a537cc0bb7e7e5a73f8d2ac478d89be2a226ecde8b97d28f61642625
|
4
|
+
data.tar.gz: dea6ce0b436d527c8d668f4e9652dd8ac18327cdfe08304bc194c753fba08b29
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8a18685dd70429d75a36578082b26656012aa79f40eda357400c22ce151cbc016ec3d0faaaf8e20097a7ec97bb43c6dfec0849ff058ddff814e277f553dee27c
|
7
|
+
data.tar.gz: 21eeeb99b5d606d7416134c95ca9acb5e2f7b5547ed9cf18d165b4ae0047a5315383c234c828fd8b0c87663583ef7569dd729ea5d038fae7c1ad13978a1c200a
|
data/README.md
CHANGED
@@ -1,21 +1,102 @@
|
|
1
|
-
# Aspire Budget - Ruby
|
1
|
+
# Aspire Budget - Ruby
|
2
2
|
|
3
|
-
|
3
|
+
[](https://github.com/drowze/aspirebudgeting_ruby)
|
4
|
+
[](https://github.com/rubocop-hq/rubocop)
|
5
|
+
[](https://codecov.io/gh/Drowze/aspirebudgeting_ruby)
|
6
|
+
[](https://codeclimate.com/github/Drowze/aspirebudgeting_ruby/maintainability)
|
7
|
+
[](https://codeclimate.com/github/Drowze/aspirebudgeting_ruby/test_coverage)
|
8
|
+
|
9
|
+
This is an independent project implementing a Ruby for Aspire Budgeting spreadsheets, leveraging from the use of another great gem: `google_drive`.
|
4
10
|
The idea of this gem is to enable a good API to be easily implemented, allowing more powerful and complex tools to emerge.
|
5
11
|
|
6
12
|
If you don't know Aspire Budgeting please refer to: https://aspirebudget.com/
|
7
13
|
|
8
14
|
## Installation
|
9
15
|
|
10
|
-
|
16
|
+
```bash
|
17
|
+
$ gem install aspire_budget
|
18
|
+
```
|
11
19
|
|
12
20
|
## Usage
|
13
21
|
|
22
|
+
Either have an initializer with your config:
|
23
|
+
|
14
24
|
```ruby
|
15
|
-
|
16
|
-
|
25
|
+
# Use this method if you plan to work on a single spreadsheet with your application
|
26
|
+
AspireBudget.configure do |config|
|
27
|
+
config.session = GoogleDrive.from_config('path_to_your_credentials.json')
|
28
|
+
config.spreadsheet_key = 'YOUR_SPREADSHEET_KEY'
|
29
|
+
end
|
17
30
|
```
|
18
31
|
|
32
|
+
Or specify the config when initializing a worksheet like below.
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
# Use this method if you plan working with multiple spreadsheets with your application
|
36
|
+
AspireBudget::Worksheets::Transactions.new(
|
37
|
+
session: GoogleDrive.from_config('path_to_your_credentials.json'),
|
38
|
+
spreadsheet_key: 'YOUR_SPREADSHEET_KEY'
|
39
|
+
)
|
40
|
+
```
|
41
|
+
|
42
|
+
List transactions:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
# or AspireBudget::Worksheets::Transactions.new(...).all
|
46
|
+
AspireBudget::Worksheets::Transactions.all
|
47
|
+
=> #[<AspireBudget::Models::Transaction:0x0000564acc1ae088
|
48
|
+
# @account="Revolut",
|
49
|
+
# @category="Groceries",
|
50
|
+
# @date=#<Date: 2020-05-31 ((2459001j,0s,0n),+0s,2299161j)>,
|
51
|
+
# @inflow=0.0,
|
52
|
+
# @memo="Tesco",
|
53
|
+
# @outflow=22.51,
|
54
|
+
# @status=:approved>,
|
55
|
+
# <AspireBudget::Models::Transaction:0x0000564acc1541a0
|
56
|
+
# @account="Revolut",
|
57
|
+
# @category="Electric Bill",
|
58
|
+
# @date=#<Date: 2020-06-22 ((2459023j,0s,0n),+0s,2299161j)>,
|
59
|
+
# @inflow=0.0,
|
60
|
+
# @memo="Amazon",
|
61
|
+
# @outflow=21.54,
|
62
|
+
# @status=:approved>]
|
63
|
+
```
|
64
|
+
|
65
|
+
Insert transaction:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
# or AspireBudget::Worksheets::Transactions.new(...).all
|
69
|
+
# you can also pass a Transaction record instead of a hash
|
70
|
+
AspireBudget::Worksheets::Transactions.insert(date: '25/06/2020', outflow: 10.0, inflow: 12.0, category: 'test', account: 'AIB', memo: 'ruby', status: :pending)
|
71
|
+
=> #<AspireBudget::Models::Transaction:0x0000564acc1522b0 ... >
|
72
|
+
```
|
73
|
+
|
74
|
+
List category transfers
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
AspireBudget::Worksheets::CategoryTransfers.all
|
78
|
+
=> [#<AspireBudget::Models::CategoryTransfer:0x0000559501fddab8
|
79
|
+
# @amount=46.37,
|
80
|
+
# @date=#<Date: 2020-06-29 ((2459030j,0s,0n),+0s,2299161j)>,
|
81
|
+
# @from="Available to budget",
|
82
|
+
# @to="Lunch / Breakfast out",
|
83
|
+
# @memo="Monthly target">,
|
84
|
+
# <AspireBudget::Models::CategoryTransfer:0x0000559501fdd810
|
85
|
+
# @amount=15.0,
|
86
|
+
# @date=#<Date: 2020-06-29 ((2459030j,0s,0n),+0s,2299161j)>,
|
87
|
+
# @from="Public Transport",
|
88
|
+
# @memo="",
|
89
|
+
# @to="Available to budget">]
|
90
|
+
```
|
91
|
+
|
92
|
+
Insert category transfer
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
AspireBudget::Worksheets::CategoryTransfers.insert(amount: 10, date: '25/06/2020', from: 'Available to budget', to: 'Electric Bill', memo: 'test')
|
96
|
+
=> #<AspireBudget::Models::CategoryTransfer:0x0000264acc1529b0 ... >
|
97
|
+
```
|
98
|
+
|
99
|
+
|
19
100
|
## Development
|
20
101
|
|
21
102
|
todo
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'aspire_budget/core_extensions'
|
4
|
+
|
5
|
+
require_relative 'aspire_budget/version'
|
6
|
+
require_relative 'aspire_budget/configuration'
|
7
|
+
|
8
|
+
require_relative 'aspire_budget/worksheets/backend_data'
|
9
|
+
require_relative 'aspire_budget/worksheets/transactions'
|
10
|
+
require_relative 'aspire_budget/worksheets/category_transfers'
|
11
|
+
|
12
|
+
require_relative 'aspire_budget/models/transaction'
|
13
|
+
require_relative 'aspire_budget/models/category_transfer'
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AspireBudget
|
4
|
+
# Configures default values
|
5
|
+
def self.configure
|
6
|
+
yield(configuration) if block_given?
|
7
|
+
end
|
8
|
+
|
9
|
+
# @return [AspireBudget::Configuration] the current configured defaults
|
10
|
+
def self.configuration
|
11
|
+
Thread.current[:aspire_budget_configuration] ||= Configuration.new
|
12
|
+
end
|
13
|
+
|
14
|
+
# Overwrite the current configured defaults
|
15
|
+
# @param [AspireBudget::Configuration]
|
16
|
+
def self.configuration=(other)
|
17
|
+
Thread.current[:aspire_budget_configuration] = other
|
18
|
+
end
|
19
|
+
|
20
|
+
# Resets the set configuration. Useful on e.g. testing
|
21
|
+
def self.reset!
|
22
|
+
Thread.current[:aspire_budget_configuration] = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
class Configuration
|
26
|
+
# Authenticated GoogleDrive session
|
27
|
+
# @return [GoogleDrive::Session]
|
28
|
+
attr_accessor :session
|
29
|
+
|
30
|
+
# Google spreadsheet key (as it is in the url)
|
31
|
+
# @return [String]
|
32
|
+
attr_accessor :spreadsheet_key
|
33
|
+
|
34
|
+
# Build an agent using given +session+ and +spreadsheet_key+ (falling back
|
35
|
+
# to the configured ones).
|
36
|
+
# @return [GoogleDrive::Spreadsheet] an spreadsheet agent
|
37
|
+
# @param session [GoogleDrive::Session] will fallback to configured one if
|
38
|
+
# not defined
|
39
|
+
# @param spreadsheet_key [String] will fallback to configured one if not
|
40
|
+
# defined
|
41
|
+
def agent(session = nil, spreadsheet_key = nil)
|
42
|
+
@agents ||= Hash.new do |h, k|
|
43
|
+
h[k] = k.first.spreadsheet_by_key(k.last)
|
44
|
+
end
|
45
|
+
@agents[
|
46
|
+
[session || self.session, spreadsheet_key || self.spreadsheet_key]
|
47
|
+
]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'google_drive'
|
4
|
+
|
5
|
+
module AspireBudget
|
6
|
+
module CoreExtensions
|
7
|
+
# rubocop:disable all
|
8
|
+
module Worksheet
|
9
|
+
def rows(skip = 0)
|
10
|
+
nc = num_cols
|
11
|
+
result = ((1 + skip)..num_rows).map do |row|
|
12
|
+
(1..nc).map do |col|
|
13
|
+
block_given? ? yield(row, col) : self[row, col]
|
14
|
+
end.freeze
|
15
|
+
end
|
16
|
+
result.freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
# Same as +#rows+, but replacing cells with numeric values where they exist.
|
20
|
+
# Please note that this will NOT replace dirty cells with their numeric
|
21
|
+
# value.
|
22
|
+
#
|
23
|
+
# @see #rows
|
24
|
+
# @see #numeric_value
|
25
|
+
def rows_with_numerics(skip = 0)
|
26
|
+
rows(skip) { |row, col| numeric_value(row, col) || self[row, col] }
|
27
|
+
end
|
28
|
+
|
29
|
+
def []=(*args) # rubocop:disable all
|
30
|
+
(row, col) = parse_cell_args(args[0...-1])
|
31
|
+
value = args[-1]
|
32
|
+
|
33
|
+
reload_cells unless @cells
|
34
|
+
@numeric_values[[row, col]] = value.is_a?(Numeric) ? value : nil
|
35
|
+
value = value.to_s
|
36
|
+
validate_cell_value(value)
|
37
|
+
|
38
|
+
@cells[[row, col]] = value
|
39
|
+
@input_values[[row, col]] = value
|
40
|
+
@modified.add([row, col])
|
41
|
+
self.max_rows = row if row > @max_rows
|
42
|
+
self.max_cols = col if col > @max_cols
|
43
|
+
if value.empty?
|
44
|
+
@num_rows = nil
|
45
|
+
@num_cols = nil
|
46
|
+
else
|
47
|
+
@num_rows = row if @num_rows && row > @num_rows
|
48
|
+
@num_cols = col if @num_cols && col > @num_cols
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def save # rubocop:disable all
|
53
|
+
sent = false
|
54
|
+
|
55
|
+
if @meta_modified
|
56
|
+
add_request({
|
57
|
+
update_sheet_properties: {
|
58
|
+
properties: {
|
59
|
+
sheet_id: sheet_id,
|
60
|
+
title: title,
|
61
|
+
index: index,
|
62
|
+
grid_properties: {row_count: max_rows, column_count: max_cols},
|
63
|
+
},
|
64
|
+
fields: '*',
|
65
|
+
},
|
66
|
+
})
|
67
|
+
end
|
68
|
+
|
69
|
+
if !@v4_requests.empty?
|
70
|
+
self.spreadsheet.batch_update(@v4_requests)
|
71
|
+
@v4_requests = []
|
72
|
+
sent = true
|
73
|
+
end
|
74
|
+
|
75
|
+
@remote_title = @title
|
76
|
+
|
77
|
+
unless @modified.empty?
|
78
|
+
min_modified_row = 1.0 / 0.0
|
79
|
+
max_modified_row = 0
|
80
|
+
min_modified_col = 1.0 / 0.0
|
81
|
+
max_modified_col = 0
|
82
|
+
@modified.each do |r, c|
|
83
|
+
min_modified_row = r if r < min_modified_row
|
84
|
+
max_modified_row = r if r > max_modified_row
|
85
|
+
min_modified_col = c if c < min_modified_col
|
86
|
+
max_modified_col = c if c > max_modified_col
|
87
|
+
end
|
88
|
+
|
89
|
+
# Uses update_spreadsheet_value instead batch_update_spreadsheet with
|
90
|
+
# update_cells. batch_update_spreadsheet has benefit that the request
|
91
|
+
# can be batched with other requests. But it has drawback that the
|
92
|
+
# type of the value (string_value, number_value, etc.) must be
|
93
|
+
# explicitly specified in user_entered_value. Since I don't know exact
|
94
|
+
# logic to determine the type from text, I chose to use
|
95
|
+
# update_spreadsheet_value here.
|
96
|
+
range = "'%s'!R%dC%d:R%dC%d" %
|
97
|
+
[@title, min_modified_row, min_modified_col, max_modified_row, max_modified_col]
|
98
|
+
values = (min_modified_row..max_modified_row).map do |r|
|
99
|
+
(min_modified_col..max_modified_col).map do |c|
|
100
|
+
next unless @modified.include?([r, c])
|
101
|
+
|
102
|
+
@numeric_values[[r, c]] || @cells[[r, c]] || ''
|
103
|
+
end
|
104
|
+
end
|
105
|
+
value_range = Google::Apis::SheetsV4::ValueRange.new(values: values)
|
106
|
+
@session.sheets_service.update_spreadsheet_value(
|
107
|
+
spreadsheet.id, range, value_range, value_input_option: 'USER_ENTERED')
|
108
|
+
|
109
|
+
@modified.clear
|
110
|
+
sent = true
|
111
|
+
end
|
112
|
+
|
113
|
+
sent
|
114
|
+
end
|
115
|
+
end
|
116
|
+
# rubocop:enable all
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# https://github.com/gimite/google-drive-ruby/issues/378 (PR #377)
|
121
|
+
# https://github.com/gimite/google-drive-ruby/issues/380 (PR #379)
|
122
|
+
GoogleDrive::Worksheet.prepend AspireBudget::CoreExtensions::Worksheet
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../utils'
|
4
|
+
|
5
|
+
module AspireBudget
|
6
|
+
module Models
|
7
|
+
class CategoryTransfer
|
8
|
+
attr_reader :date, :amount, :from, :to, :memo
|
9
|
+
|
10
|
+
def self.from_row(header, row)
|
11
|
+
params = header.zip(row).to_h
|
12
|
+
|
13
|
+
params.tap do |h|
|
14
|
+
h[:date] = Utils.parse_date(h[:date])
|
15
|
+
end
|
16
|
+
|
17
|
+
new(**params)
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(date:, amount:, from:, to:, memo:)
|
21
|
+
@date = date.nil? ? Date.today : Utils.parse_date(date)
|
22
|
+
@amount = amount.to_f
|
23
|
+
@from = from || 'Available to Budget'
|
24
|
+
@to = to
|
25
|
+
@memo = memo
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_row(header)
|
29
|
+
header.map do |h|
|
30
|
+
value = send(h)
|
31
|
+
next Utils.serialize_date(value) if h == :date
|
32
|
+
|
33
|
+
value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative '../utils'
|
4
4
|
|
5
5
|
module AspireBudget
|
6
6
|
module Models
|
@@ -12,30 +12,29 @@ module AspireBudget
|
|
12
12
|
|
13
13
|
params.tap do |h|
|
14
14
|
h[:date] = Utils.parse_date(h[:date])
|
15
|
-
h[:outflow] = Utils.parse_currency(h[:outflow])
|
16
|
-
h[:inflow] = Utils.parse_currency(h[:inflow])
|
17
15
|
h[:status] = Utils.parse_status(h[:status])
|
18
16
|
end
|
19
17
|
|
20
18
|
new(**params)
|
21
19
|
end
|
22
20
|
|
21
|
+
# rubocop:disable Metrics/ParameterLists
|
23
22
|
def initialize(date:, outflow:, inflow:, category:, account:, memo:, status:)
|
24
|
-
@date = Utils.parse_date(date)
|
25
|
-
@outflow = outflow
|
26
|
-
@inflow = inflow
|
23
|
+
@date = date.nil? ? Date.today : Utils.parse_date(date)
|
24
|
+
@outflow = outflow.to_f
|
25
|
+
@inflow = inflow.to_f
|
27
26
|
@category = category
|
28
27
|
@account = account
|
29
28
|
@memo = memo
|
30
29
|
@status = status
|
31
30
|
end
|
31
|
+
# rubocop:enable Metrics/ParameterLists
|
32
32
|
|
33
33
|
def to_row(header)
|
34
34
|
header.map do |h|
|
35
35
|
value = send(h)
|
36
36
|
next Utils.serialize_date(value) if h == :date
|
37
37
|
next Utils.serialize_status(value) if h == :status
|
38
|
-
next Utils.serialize_currency(value) if %i[inflow outflow].include?(h)
|
39
38
|
|
40
39
|
value
|
41
40
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
module AspireBudget
|
6
|
+
module Utils
|
7
|
+
class << self
|
8
|
+
# Parses a value to a date object
|
9
|
+
# @return [Date]
|
10
|
+
# @param value either a numeric object or an object responding to +to_date+
|
11
|
+
def parse_date(value)
|
12
|
+
return parse_serial_date(value) if value.is_a?(Numeric)
|
13
|
+
return value.to_date if value.respond_to?(:to_date)
|
14
|
+
|
15
|
+
raise 'Unsupported date format'
|
16
|
+
end
|
17
|
+
|
18
|
+
# Parses a value to a serial date
|
19
|
+
# @return [Float]
|
20
|
+
# @param value either a numeric object or an object responding to +to_date+
|
21
|
+
def serialize_date(value)
|
22
|
+
return Float(value) if value.is_a?(Numeric) && value >= 0
|
23
|
+
|
24
|
+
value = value.to_date if value.respond_to?(:to_date)
|
25
|
+
raise 'Unsupported date value' unless value.is_a?(Date)
|
26
|
+
raise "Date should be after #{LOTUS_DAY_ONE}" if LOTUS_DAY_ONE > value
|
27
|
+
|
28
|
+
Float(value - LOTUS_DAY_ONE)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Parses a status icon
|
32
|
+
# @return [Symbol]
|
33
|
+
# @param value [String]
|
34
|
+
def parse_status(value)
|
35
|
+
TRANSACTION_STATUS_MAPPING.fetch(value, nil)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Serialize a status symbol
|
39
|
+
# @return [String]
|
40
|
+
# @param value [Symbol]
|
41
|
+
def serialize_status(value)
|
42
|
+
TRANSACTION_STATUS_MAPPING.key(value) || ''
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
TRANSACTION_STATUS_MAPPING = {
|
48
|
+
'✅' => :approved,
|
49
|
+
'🅿️' => :pending,
|
50
|
+
'*️⃣' => :reconciliation
|
51
|
+
}.freeze
|
52
|
+
private_constant :TRANSACTION_STATUS_MAPPING
|
53
|
+
|
54
|
+
LOTUS_DAY_ONE = Date.new(1899, 12, 30).freeze
|
55
|
+
private_constant :LOTUS_DAY_ONE
|
56
|
+
|
57
|
+
def parse_serial_date(days_after_lotus_day_one)
|
58
|
+
LOTUS_DAY_ONE + days_after_lotus_day_one
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'worksheet_base'
|
4
|
+
|
5
|
+
module AspireBudget
|
6
|
+
module Worksheets
|
7
|
+
class BackendData < WorksheetBase
|
8
|
+
WS_TITLE = 'BackendData'
|
9
|
+
|
10
|
+
# @return [String] the spreadsheet version
|
11
|
+
def version
|
12
|
+
version_column = ws.rows[0].index { |header| header.include?('Update') }
|
13
|
+
ws.rows[1][version_column]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'worksheet_base'
|
4
|
+
require_relative 'transactions'
|
5
|
+
require_relative '../models/category_transfer'
|
6
|
+
|
7
|
+
module AspireBudget
|
8
|
+
module Worksheets
|
9
|
+
class CategoryTransfers < Transactions
|
10
|
+
WS_TITLE = 'Category Transfers'
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def klass
|
15
|
+
Models::CategoryTransfer
|
16
|
+
end
|
17
|
+
|
18
|
+
def header
|
19
|
+
@header ||=
|
20
|
+
super.map { |k| k == :'from category' ? :from : k }
|
21
|
+
.map { |k| k == :'to category' ? :to : k }[0...-1]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'worksheet_base'
|
4
|
+
require_relative '../models/transaction'
|
5
|
+
|
6
|
+
module AspireBudget
|
7
|
+
module Worksheets
|
8
|
+
class Transactions < WorksheetBase
|
9
|
+
WS_TITLE = 'Transactions'
|
10
|
+
|
11
|
+
# @return [Array<AspireBudget::Transaction>] all transactions
|
12
|
+
def all
|
13
|
+
rows.map do |row|
|
14
|
+
klass.from_row(header, row)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Inserts a transaction to the spreadsheet. Accepts either a transaction
|
19
|
+
# record or a hash (that is passed to the transaction initializer)
|
20
|
+
# @see AspireBudget::Models::Transaction#initialize
|
21
|
+
# @param record [AspireBudget::Transaction, Hash]
|
22
|
+
# @return [AspireBudget::Transaction] a transaction
|
23
|
+
def insert(record, sync: true)
|
24
|
+
record = klass.new(**record) if record.is_a?(Hash)
|
25
|
+
row = record.to_row(header)
|
26
|
+
ws.update_cells(*next_row_col, [row])
|
27
|
+
ws.synchronize if sync
|
28
|
+
record
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def klass
|
34
|
+
Models::Transaction
|
35
|
+
end
|
36
|
+
|
37
|
+
# There is a 1 cell margin before the spreadsheet content
|
38
|
+
def margin_left
|
39
|
+
1
|
40
|
+
end
|
41
|
+
|
42
|
+
def next_row_col
|
43
|
+
[ws.num_rows + 1, margin_left + 1]
|
44
|
+
end
|
45
|
+
|
46
|
+
def sanitize(row)
|
47
|
+
return if row.all? { |cell| cell == '' }
|
48
|
+
|
49
|
+
row.drop(margin_left)
|
50
|
+
end
|
51
|
+
|
52
|
+
def rows
|
53
|
+
ws.rows_with_numerics(header_location)
|
54
|
+
.map(&method(:sanitize)).compact
|
55
|
+
end
|
56
|
+
|
57
|
+
def header
|
58
|
+
@header ||=
|
59
|
+
ws.rows(header_location - 1)
|
60
|
+
.first
|
61
|
+
.drop(margin_left)
|
62
|
+
.map(&:downcase)
|
63
|
+
.map(&:to_sym)
|
64
|
+
end
|
65
|
+
|
66
|
+
def header_location
|
67
|
+
@header_location ||=
|
68
|
+
(1..ws.num_rows).find { |i| ws[i, margin_left + 1].casecmp?('date') }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../configuration'
|
4
|
+
|
5
|
+
module AspireBudget
|
6
|
+
module Worksheets
|
7
|
+
# @abstract Subclass and reimplement ws_title to implement a custom
|
8
|
+
# worksheet
|
9
|
+
class WorksheetBase
|
10
|
+
class << self
|
11
|
+
# @return an instance of the current object
|
12
|
+
def instance
|
13
|
+
Thread.current[to_s] ||= new
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def method_missing(method_name, *args, &block)
|
19
|
+
if instance.respond_to?(method_name)
|
20
|
+
instance.public_send(method_name, *args, &block)
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def respond_to_missing?(method_name, _include_private)
|
27
|
+
instance.respond_to?(method_name) || super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# @see AspireBudget::Configuration#agent
|
32
|
+
# @return a new instance of the calling class configured with an agent
|
33
|
+
# @param session [GoogleDrive::Session]
|
34
|
+
# @param spreadsheet_key [String] spreadsheet key as per its url
|
35
|
+
def initialize(session: nil, spreadsheet_key: nil)
|
36
|
+
@session = session
|
37
|
+
@spreadsheet_key = spreadsheet_key
|
38
|
+
@agent = AspireBudget.configuration.agent(@session, @spreadsheet_key)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Boolean] Whether the worksheet has unsaved changes
|
42
|
+
def dirty?
|
43
|
+
ws.dirty?
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def ws
|
49
|
+
worksheets[self.class::WS_TITLE]
|
50
|
+
end
|
51
|
+
|
52
|
+
def worksheets
|
53
|
+
@worksheets ||=
|
54
|
+
@agent.worksheets.reduce({}) { |h, sheet| h.merge(sheet.title => sheet) }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aspire_budget
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Drowze
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: google_drive
|
@@ -24,37 +24,188 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '3.0'
|
27
|
-
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry
|
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: pry-byebug
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.9.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.9.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '13.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '13.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.89.1
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.89.1
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop-packaging
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.2.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.2.0
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop-performance
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 1.7.1
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 1.7.1
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubocop-rspec
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 1.42.0
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 1.42.0
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rubocop-thread_safety
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 0.4.1
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 0.4.1
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: simplecov
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: 0.17.0
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: 0.17.0
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: webmock
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
description: |2
|
182
|
+
Aspire Budget is a free zero-based envelope-style budgeting spreadsheet
|
183
|
+
built with Google Sheets by Matthew Alcorn. This gem aims to provide an
|
184
|
+
expressive Ruby interface to it.
|
28
185
|
email:
|
29
186
|
- gibim6+aspire@gmail.com
|
30
187
|
executables: []
|
31
188
|
extensions: []
|
32
189
|
extra_rdoc_files: []
|
33
190
|
files:
|
34
|
-
- ".gitignore"
|
35
|
-
- ".rspec"
|
36
|
-
- ".rubocop.yml"
|
37
|
-
- ".travis.yml"
|
38
|
-
- Gemfile
|
39
|
-
- Gemfile.lock
|
40
191
|
- LICENSE.txt
|
41
192
|
- README.md
|
42
|
-
-
|
43
|
-
- aspire_budget.
|
44
|
-
-
|
45
|
-
-
|
46
|
-
- lib/
|
47
|
-
- lib/
|
48
|
-
- lib/
|
49
|
-
- lib/
|
50
|
-
- lib/worksheets/
|
51
|
-
- lib/worksheets/transactions.rb
|
52
|
-
- lib/worksheets/worksheet_base.rb
|
53
|
-
homepage: https://
|
193
|
+
- lib/aspire_budget.rb
|
194
|
+
- lib/aspire_budget/configuration.rb
|
195
|
+
- lib/aspire_budget/core_extensions.rb
|
196
|
+
- lib/aspire_budget/models/category_transfer.rb
|
197
|
+
- lib/aspire_budget/models/transaction.rb
|
198
|
+
- lib/aspire_budget/utils.rb
|
199
|
+
- lib/aspire_budget/version.rb
|
200
|
+
- lib/aspire_budget/worksheets/backend_data.rb
|
201
|
+
- lib/aspire_budget/worksheets/category_transfers.rb
|
202
|
+
- lib/aspire_budget/worksheets/transactions.rb
|
203
|
+
- lib/aspire_budget/worksheets/worksheet_base.rb
|
204
|
+
homepage: https://github.com/drowze/aspirebudgeting_ruby
|
54
205
|
licenses:
|
55
206
|
- MIT
|
56
207
|
metadata:
|
57
|
-
homepage_uri: https://
|
208
|
+
homepage_uri: https://github.com/drowze/aspirebudgeting_ruby
|
58
209
|
source_code_uri: https://github.com/drowze/aspirebudgeting_ruby
|
59
210
|
post_install_message:
|
60
211
|
rdoc_options: []
|
@@ -64,13 +215,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
215
|
requirements:
|
65
216
|
- - ">="
|
66
217
|
- !ruby/object:Gem::Version
|
67
|
-
version: 2.
|
218
|
+
version: 2.4.0
|
68
219
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
220
|
requirements:
|
70
221
|
- - ">="
|
71
222
|
- !ruby/object:Gem::Version
|
72
223
|
version: '0'
|
73
|
-
requirements:
|
224
|
+
requirements:
|
225
|
+
- Aspire Budget spreadsheet v3.1.0+
|
74
226
|
rubygems_version: 3.1.2
|
75
227
|
signing_key:
|
76
228
|
specification_version: 4
|
data/.gitignore
DELETED
data/.rspec
DELETED
data/.rubocop.yml
DELETED
data/.travis.yml
DELETED
data/Gemfile
DELETED
data/Gemfile.lock
DELETED
@@ -1,109 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
aspire_budget (0.0.1)
|
5
|
-
google_drive (~> 3.0)
|
6
|
-
|
7
|
-
GEM
|
8
|
-
remote: https://rubygems.org/
|
9
|
-
specs:
|
10
|
-
addressable (2.7.0)
|
11
|
-
public_suffix (>= 2.0.2, < 5.0)
|
12
|
-
ast (2.4.1)
|
13
|
-
coderay (1.1.3)
|
14
|
-
declarative (0.0.10)
|
15
|
-
declarative-option (0.1.0)
|
16
|
-
diff-lcs (1.4.1)
|
17
|
-
faraday (1.0.1)
|
18
|
-
multipart-post (>= 1.2, < 3)
|
19
|
-
google-api-client (0.41.0)
|
20
|
-
addressable (~> 2.5, >= 2.5.1)
|
21
|
-
googleauth (~> 0.9)
|
22
|
-
httpclient (>= 2.8.1, < 3.0)
|
23
|
-
mini_mime (~> 1.0)
|
24
|
-
representable (~> 3.0)
|
25
|
-
retriable (>= 2.0, < 4.0)
|
26
|
-
signet (~> 0.12)
|
27
|
-
google_drive (3.0.5)
|
28
|
-
google-api-client (>= 0.11.0, < 1.0.0)
|
29
|
-
googleauth (>= 0.5.0, < 1.0.0)
|
30
|
-
nokogiri (>= 1.5.3, < 2.0.0)
|
31
|
-
googleauth (0.13.0)
|
32
|
-
faraday (>= 0.17.3, < 2.0)
|
33
|
-
jwt (>= 1.4, < 3.0)
|
34
|
-
memoist (~> 0.16)
|
35
|
-
multi_json (~> 1.11)
|
36
|
-
os (>= 0.9, < 2.0)
|
37
|
-
signet (~> 0.14)
|
38
|
-
httpclient (2.8.3)
|
39
|
-
jwt (2.2.1)
|
40
|
-
memoist (0.16.2)
|
41
|
-
method_source (1.0.0)
|
42
|
-
mini_mime (1.0.2)
|
43
|
-
mini_portile2 (2.4.0)
|
44
|
-
multi_json (1.14.1)
|
45
|
-
multipart-post (2.1.1)
|
46
|
-
nokogiri (1.10.9)
|
47
|
-
mini_portile2 (~> 2.4.0)
|
48
|
-
os (1.1.0)
|
49
|
-
parallel (1.19.2)
|
50
|
-
parser (2.7.1.4)
|
51
|
-
ast (~> 2.4.1)
|
52
|
-
pry (0.13.1)
|
53
|
-
coderay (~> 1.1)
|
54
|
-
method_source (~> 1.0)
|
55
|
-
public_suffix (4.0.5)
|
56
|
-
rainbow (3.0.0)
|
57
|
-
rake (12.3.3)
|
58
|
-
regexp_parser (1.7.1)
|
59
|
-
representable (3.0.4)
|
60
|
-
declarative (< 0.1.0)
|
61
|
-
declarative-option (< 0.2.0)
|
62
|
-
uber (< 0.2.0)
|
63
|
-
retriable (3.1.2)
|
64
|
-
rexml (3.2.4)
|
65
|
-
rspec (3.9.0)
|
66
|
-
rspec-core (~> 3.9.0)
|
67
|
-
rspec-expectations (~> 3.9.0)
|
68
|
-
rspec-mocks (~> 3.9.0)
|
69
|
-
rspec-core (3.9.2)
|
70
|
-
rspec-support (~> 3.9.3)
|
71
|
-
rspec-expectations (3.9.2)
|
72
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
73
|
-
rspec-support (~> 3.9.0)
|
74
|
-
rspec-mocks (3.9.1)
|
75
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
76
|
-
rspec-support (~> 3.9.0)
|
77
|
-
rspec-support (3.9.3)
|
78
|
-
rubocop (0.86.0)
|
79
|
-
parallel (~> 1.10)
|
80
|
-
parser (>= 2.7.0.1)
|
81
|
-
rainbow (>= 2.2.2, < 4.0)
|
82
|
-
regexp_parser (>= 1.7)
|
83
|
-
rexml
|
84
|
-
rubocop-ast (>= 0.0.3, < 1.0)
|
85
|
-
ruby-progressbar (~> 1.7)
|
86
|
-
unicode-display_width (>= 1.4.0, < 2.0)
|
87
|
-
rubocop-ast (0.0.3)
|
88
|
-
parser (>= 2.7.0.1)
|
89
|
-
ruby-progressbar (1.10.1)
|
90
|
-
signet (0.14.0)
|
91
|
-
addressable (~> 2.3)
|
92
|
-
faraday (>= 0.17.3, < 2.0)
|
93
|
-
jwt (>= 1.5, < 3.0)
|
94
|
-
multi_json (~> 1.10)
|
95
|
-
uber (0.1.0)
|
96
|
-
unicode-display_width (1.7.0)
|
97
|
-
|
98
|
-
PLATFORMS
|
99
|
-
ruby
|
100
|
-
|
101
|
-
DEPENDENCIES
|
102
|
-
aspire_budget!
|
103
|
-
pry
|
104
|
-
rake (~> 12.0)
|
105
|
-
rspec (~> 3.0)
|
106
|
-
rubocop
|
107
|
-
|
108
|
-
BUNDLED WITH
|
109
|
-
2.1.4
|
data/Rakefile
DELETED
data/aspire_budget.gemspec
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'lib/version'
|
4
|
-
|
5
|
-
Gem::Specification.new do |spec|
|
6
|
-
spec.name = 'aspire_budget'
|
7
|
-
spec.version = AspireBudget::VERSION
|
8
|
-
spec.authors = ['Drowze']
|
9
|
-
spec.email = ['gibim6+aspire@gmail.com']
|
10
|
-
|
11
|
-
spec.summary = 'Aspire Budget Ruby Wrapper'
|
12
|
-
spec.description = 'Aspire Budget Ruby Wrapper'
|
13
|
-
spec.homepage = 'https://google.com'
|
14
|
-
spec.license = 'MIT'
|
15
|
-
spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
|
16
|
-
|
17
|
-
spec.metadata['homepage_uri'] = spec.homepage
|
18
|
-
spec.metadata['source_code_uri'] = 'https://github.com/drowze/aspirebudgeting_ruby'
|
19
|
-
|
20
|
-
# Specify which files should be added to the gem when it is released.
|
21
|
-
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
-
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
23
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
24
|
-
end
|
25
|
-
spec.bindir = 'exe'
|
26
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
|
-
spec.require_paths = ['lib']
|
28
|
-
|
29
|
-
spec.add_runtime_dependency 'google_drive', '~> 3.0'
|
30
|
-
end
|
data/bin/console
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
require 'pry'
|
5
|
-
require 'google_drive'
|
6
|
-
|
7
|
-
require 'bundler/setup'
|
8
|
-
require 'client'
|
9
|
-
|
10
|
-
session = GoogleDrive::Session.from_config(ENV['GOOGLE_CREDENTIALS_PATH'])
|
11
|
-
client = AspireBudget::Client.new(session: session, spreadsheet_key: ENV['SPREADSHEET_KEY'])
|
12
|
-
|
13
|
-
binding.pry
|
14
|
-
|
15
|
-
# params = { date: '25/06/2020', outflow: 10.0, inflow: 12.0, category: 'test', account: 'AIB', memo: 'ruby', status: :pending }
|
16
|
-
# client.insert_transaction(params)
|
17
|
-
|
18
|
-
# client.transactions_list
|
data/bin/setup
DELETED
data/lib/client.rb
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'worksheets/backend_data'
|
4
|
-
require 'worksheets/transactions'
|
5
|
-
|
6
|
-
require 'models/transaction'
|
7
|
-
|
8
|
-
module AspireBudget
|
9
|
-
class Client
|
10
|
-
def initialize(session:, spreadsheet_key:)
|
11
|
-
@session = session
|
12
|
-
@spreadsheet_key = spreadsheet_key
|
13
|
-
end
|
14
|
-
|
15
|
-
def categories
|
16
|
-
backend_data.categories
|
17
|
-
end
|
18
|
-
|
19
|
-
def transaction_list
|
20
|
-
transactions.all
|
21
|
-
end
|
22
|
-
|
23
|
-
def insert_transaction(params)
|
24
|
-
transaction = Models::Transaction.new(params)
|
25
|
-
transactions.insert(transaction)
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
def transactions
|
31
|
-
@transactions ||= Worksheets::Transactions.new(agent: agent)
|
32
|
-
end
|
33
|
-
|
34
|
-
def backend_data
|
35
|
-
@backend_data ||= Worksheets::BackendData.new(agent: agent)
|
36
|
-
end
|
37
|
-
|
38
|
-
def agent
|
39
|
-
@agent ||= begin
|
40
|
-
@session.spreadsheet_by_key(@spreadsheet_key)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
data/lib/utils.rb
DELETED
@@ -1,43 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module AspireBudget
|
4
|
-
module Utils
|
5
|
-
class << self
|
6
|
-
DATE_FORMAT = '%d/%m/%y'
|
7
|
-
CURRENCY_SYMBOL = '€'
|
8
|
-
TRANSACTION_STATUS_MAPPING = {
|
9
|
-
'✅' => :approved,
|
10
|
-
'🅿️' => :pending,
|
11
|
-
'*️⃣' => :reconciliation
|
12
|
-
}.freeze
|
13
|
-
|
14
|
-
def parse_date(value)
|
15
|
-
return value unless value.is_a?(String)
|
16
|
-
|
17
|
-
Date.strptime(value, DATE_FORMAT)
|
18
|
-
end
|
19
|
-
|
20
|
-
def serialize_date(value)
|
21
|
-
return value unless value.respond_to?(:strftime)
|
22
|
-
|
23
|
-
value.strftime(DATE_FORMAT)
|
24
|
-
end
|
25
|
-
|
26
|
-
def parse_currency(value)
|
27
|
-
value && value[/\d+\.\d+/].to_f || 0.0
|
28
|
-
end
|
29
|
-
|
30
|
-
def serialize_currency(value)
|
31
|
-
"#{CURRENCY_SYMBOL}#{format('%.2f', value)}"
|
32
|
-
end
|
33
|
-
|
34
|
-
def parse_status(value)
|
35
|
-
TRANSACTION_STATUS_MAPPING.fetch(value)
|
36
|
-
end
|
37
|
-
|
38
|
-
def serialize_status(value)
|
39
|
-
TRANSACTION_STATUS_MAPPING.key(value)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
data/lib/version.rb
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative './worksheet_base'
|
4
|
-
|
5
|
-
module AspireBudget
|
6
|
-
module Worksheets
|
7
|
-
class BackendData < WorksheetBase
|
8
|
-
WS_TITLE = 'BackendData'
|
9
|
-
|
10
|
-
def categories
|
11
|
-
fetch_data(data_title: 'Categories')
|
12
|
-
end
|
13
|
-
|
14
|
-
def category_exists?(category)
|
15
|
-
categories.include?(category)
|
16
|
-
end
|
17
|
-
|
18
|
-
def fetch_data(data_title: 'Categories')
|
19
|
-
col = (1..ws.num_cols).find_index do |i|
|
20
|
-
ws[1, i] == data_title
|
21
|
-
end
|
22
|
-
|
23
|
-
ws.rows.transpose[col].reject(&:empty?) - [data_title]
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
@@ -1,53 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'worksheets/worksheet_base'
|
4
|
-
require 'models/transaction'
|
5
|
-
|
6
|
-
module AspireBudget
|
7
|
-
module Worksheets
|
8
|
-
class Transactions < WorksheetBase
|
9
|
-
WS_TITLE = 'Transactions'
|
10
|
-
MARGIN_LEFT = 1
|
11
|
-
|
12
|
-
def all
|
13
|
-
transaction_rows.map do |row|
|
14
|
-
Models::Transaction.from_row(transactions_header, row)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def insert(transaction, sync: true)
|
19
|
-
row = transaction.to_row(transactions_header)
|
20
|
-
ws.update_cells(ws.rows.size + 1, MARGIN_LEFT + 1, [row])
|
21
|
-
ws.synchronize if sync
|
22
|
-
Models::Transaction.from_row(transactions_header, sanitize(ws.rows.last))
|
23
|
-
end
|
24
|
-
|
25
|
-
private
|
26
|
-
|
27
|
-
def sanitize(row)
|
28
|
-
return if row.all?(&:empty?)
|
29
|
-
|
30
|
-
row.drop(MARGIN_LEFT)
|
31
|
-
end
|
32
|
-
|
33
|
-
def transaction_rows
|
34
|
-
ws.rows(transactions_header_location)
|
35
|
-
.map(&method(:sanitize)).compact
|
36
|
-
end
|
37
|
-
|
38
|
-
def transactions_header
|
39
|
-
@transactions_header ||=
|
40
|
-
ws.rows(transactions_header_location - 1)
|
41
|
-
.first
|
42
|
-
.drop(MARGIN_LEFT)
|
43
|
-
.map(&:downcase)
|
44
|
-
.map(&:to_sym)
|
45
|
-
end
|
46
|
-
|
47
|
-
def transactions_header_location
|
48
|
-
@transactions_header_location ||=
|
49
|
-
((MARGIN_LEFT + 1)..ws.num_rows).find { |i| ws[i, MARGIN_LEFT + 1] == 'DATE' }
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
@@ -1,26 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'utils'
|
4
|
-
|
5
|
-
module AspireBudget
|
6
|
-
module Worksheets
|
7
|
-
class WorksheetBase
|
8
|
-
include Utils
|
9
|
-
|
10
|
-
def initialize(agent:)
|
11
|
-
@agent = agent
|
12
|
-
end
|
13
|
-
|
14
|
-
private
|
15
|
-
|
16
|
-
def ws
|
17
|
-
worksheets[self.class::WS_TITLE]
|
18
|
-
end
|
19
|
-
|
20
|
-
def worksheets
|
21
|
-
@worksheets ||=
|
22
|
-
@agent.worksheets.reduce({}) { |h, sheet| h.merge(sheet.title => sheet) }
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|