aspire_budget 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![CI Status](https://github.com/drowze/aspirebudgeting_ruby/workflows/CI/badge.svg)](https://github.com/drowze/aspirebudgeting_ruby)
|
4
|
+
[![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop-hq/rubocop)
|
5
|
+
[![codecov](https://codecov.io/gh/Drowze/aspirebudgeting_ruby/branch/master/graph/badge.svg)](https://codecov.io/gh/Drowze/aspirebudgeting_ruby)
|
6
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/03531044f88452981597/maintainability)](https://codeclimate.com/github/Drowze/aspirebudgeting_ruby/maintainability)
|
7
|
+
[![Test Coverage](https://api.codeclimate.com/v1/badges/03531044f88452981597/test_coverage)](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
|