appydave-tools 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +18 -0
- data/CHANGELOG.md +14 -0
- data/bin/bank_reconciliation.rb +124 -0
- data/bin/subtitle_master.rb +113 -0
- data/lib/appydave/tools/bank_reconciliation/_doc.md +36 -0
- data/lib/appydave/tools/bank_reconciliation/clean/clean_transactions.rb +106 -0
- data/lib/appydave/tools/bank_reconciliation/clean/mapper.rb +136 -0
- data/lib/appydave/tools/bank_reconciliation/clean/read_transactions.rb +88 -0
- data/lib/appydave/tools/bank_reconciliation/models/transaction.rb +91 -0
- data/lib/appydave/tools/configuration/models/bank_reconciliation_config.rb +4 -3
- data/lib/appydave/tools/subtitle_master/_doc.md +3 -0
- data/lib/appydave/tools/subtitle_master/clean.rb +80 -0
- data/lib/appydave/tools/version.rb +1 -1
- data/lib/appydave/tools.rb +8 -0
- data/package-lock.json +2 -2
- data/package.json +1 -1
- metadata +25 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e4f0fbf0d8d8efc742e691efe1e128bb49f5a1f1423fe2b5eb7a700d9d606c24
|
4
|
+
data.tar.gz: 4a6ea3bba78bbb0c8983f462888e0c7e8c1910706e90bbb9db0bfbad99e47405
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 85058aef1eccdf67319578d1d4c929a17da0143492ba91095ab7e09702f12648174a17bcb611ab5e2951ab500386b98a7f020c5695772c488ebd782bc275e3a6
|
7
|
+
data.tar.gz: fb4b852d1dff53cb256c190e586546d5ee2bf13a675da10b0d25c6abc4d04588d541541763bff650609816677e7e3fe407c9920508dcd843a8ab19ac6c2943ab
|
data/.rubocop.yml
CHANGED
@@ -45,6 +45,9 @@ RSpec/ExampleLength:
|
|
45
45
|
|
46
46
|
Metrics/MethodLength:
|
47
47
|
Max: 25
|
48
|
+
Exclude:
|
49
|
+
- "**/spec/**/*"
|
50
|
+
- "bin/*.rb"
|
48
51
|
|
49
52
|
Layout/LineLength:
|
50
53
|
Max: 200
|
@@ -80,6 +83,7 @@ Style/EmptyMethod:
|
|
80
83
|
Metrics/ParameterLists:
|
81
84
|
Exclude:
|
82
85
|
- "**/spec/**/*"
|
86
|
+
- "**/models/**/*"
|
83
87
|
Layout/EmptyLineBetweenDefs:
|
84
88
|
Exclude:
|
85
89
|
- "**/spec/**/*"
|
@@ -118,3 +122,17 @@ RSpec/PendingWithoutReason:
|
|
118
122
|
|
119
123
|
Metrics/AbcSize:
|
120
124
|
Max: 25
|
125
|
+
Exclude:
|
126
|
+
- "bin/*"
|
127
|
+
- "lib/appydave/tools/subtitle_master/clean.rb"
|
128
|
+
Metrics/CyclomaticComplexity:
|
129
|
+
Exclude:
|
130
|
+
- "**/models/**/*"
|
131
|
+
- "lib/appydave/tools/bank_reconciliation/clean/mapper.rb"
|
132
|
+
- "lib/appydave/tools/subtitle_master/clean.rb"
|
133
|
+
Metrics/PerceivedComplexity:
|
134
|
+
Exclude:
|
135
|
+
- "**/models/**/*"
|
136
|
+
- "lib/appydave/tools/subtitle_master/clean.rb"
|
137
|
+
RSpec/MultipleMemoizedHelpers:
|
138
|
+
Enabled: false
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
# [0.7.0](https://github.com/klueless-io/appydave-tools/compare/v0.6.1...v0.7.0) (2024-05-29)
|
2
|
+
|
3
|
+
|
4
|
+
### Features
|
5
|
+
|
6
|
+
* new tool for doing bank reconciliations with chart of account matching ([9b82605](https://github.com/klueless-io/appydave-tools/commit/9b8260571f6046470d5963354ee1c80e493a0f28))
|
7
|
+
|
8
|
+
## [0.6.1](https://github.com/klueless-io/appydave-tools/compare/v0.6.0...v0.6.1) (2024-05-26)
|
9
|
+
|
10
|
+
|
11
|
+
### Bug Fixes
|
12
|
+
|
13
|
+
* improved configuration printing ([89d769d](https://github.com/klueless-io/appydave-tools/commit/89d769d0741fc75b44db90931cf981feea83027f))
|
14
|
+
|
1
15
|
# [0.6.0](https://github.com/klueless-io/appydave-tools/compare/v0.5.0...v0.6.0) (2024-05-26)
|
2
16
|
|
3
17
|
|
@@ -0,0 +1,124 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
5
|
+
|
6
|
+
require 'pry'
|
7
|
+
require 'appydave/tools'
|
8
|
+
|
9
|
+
# !/usr/bin/env ruby
|
10
|
+
# frozen_string_literal: true
|
11
|
+
|
12
|
+
# Process command line arguments for any bank reconciliation operations
|
13
|
+
class BankReconciliationCLI
|
14
|
+
def initialize
|
15
|
+
@commands = {
|
16
|
+
'clean' => method(:clean_transactions),
|
17
|
+
'process' => method(:process_transactions),
|
18
|
+
'filter' => method(:filter_transactions)
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
command, *args = ARGV
|
24
|
+
if @commands.key?(command)
|
25
|
+
@commands[command].call(args)
|
26
|
+
else
|
27
|
+
puts "Unknown command: #{command}"
|
28
|
+
print_help
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def clean_transactions(args)
|
35
|
+
options = { include: [] }
|
36
|
+
OptionParser.new do |opts|
|
37
|
+
opts.banner = 'Usage: bank_reconciliation.rb clean [options]'
|
38
|
+
|
39
|
+
opts.on('-i', '--include PATTERN', 'GLOB pattern for source transaction files') do |v|
|
40
|
+
options[:include] << v
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on('-f', '--transaction FOLDER', 'Transaction CSV folder where original banking CSV files are stored') do |v|
|
44
|
+
options[:transaction_folder] = v
|
45
|
+
end
|
46
|
+
|
47
|
+
opts.on('-o', '--output FILE', 'Output CSV file name') do |v|
|
48
|
+
options[:output] = v
|
49
|
+
end
|
50
|
+
|
51
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
52
|
+
puts opts
|
53
|
+
exit
|
54
|
+
end
|
55
|
+
end.parse!(args)
|
56
|
+
|
57
|
+
transaction_folder = options[:transaction_folder] || '/default/transaction/folder'
|
58
|
+
output_file = options[:output] || 'clean_transactions.csv'
|
59
|
+
include_patterns = options[:include].empty? ? ['*'] : options[:include]
|
60
|
+
|
61
|
+
puts "Cleaning transactions with options: #{options}"
|
62
|
+
|
63
|
+
# Ensure the clean directory exists
|
64
|
+
clean_dir = File.dirname(output_file)
|
65
|
+
FileUtils.mkdir_p(clean_dir)
|
66
|
+
|
67
|
+
# Initialize the CleanTransactions class and process the files
|
68
|
+
cleaner = Appydave::Tools::BankReconciliation::Clean::CleanTransactions.new(transaction_folder: transaction_folder)
|
69
|
+
cleaner.clean_transactions(include_patterns, output_file)
|
70
|
+
|
71
|
+
puts "Cleaning transactions with options: #{options}"
|
72
|
+
end
|
73
|
+
|
74
|
+
def process_transactions(args)
|
75
|
+
options = {}
|
76
|
+
OptionParser.new do |opts|
|
77
|
+
opts.banner = 'Usage: bank_reconciliation.rb process [options]'
|
78
|
+
opts.on('-i', '--input FILE', 'Input CSV file with transactions') { |v| options[:input] = v }
|
79
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
80
|
+
puts opts
|
81
|
+
exit
|
82
|
+
end
|
83
|
+
end.parse!(args)
|
84
|
+
|
85
|
+
# Implement processing transactions with chart of accounts lookup
|
86
|
+
puts "Processing transactions with options: #{options}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def filter_transactions(args)
|
90
|
+
options = {}
|
91
|
+
OptionParser.new do |opts|
|
92
|
+
opts.banner = 'Usage: bank_reconciliation.rb filter [options]'
|
93
|
+
opts.on('-i', '--input FILE', 'Input CSV file with processed transactions') { |v| options[:input] = v }
|
94
|
+
opts.on('-y', '--year YEAR', 'Filter by financial year') { |v| options[:year] = v }
|
95
|
+
opts.on('-b', '--begin DATE', 'Filter by dates greater than or eqaul to DDMMYY') { |v| options[:year] = v }
|
96
|
+
opts.on('-e', '--end DATE', 'Filter by dates less than or eqaul to DDMMYY') { |v| options[:year] = v }
|
97
|
+
opts.on('-c', '--codes CODES', 'Filter by chart of account codes (comma-separated)') { |v| options[:codes] = v }
|
98
|
+
opts.on('-w', '--wild TEXT', 'Wildcard text match') { |v| options[:text] = v }
|
99
|
+
opts.on('-d', '--display', 'Display filtered transactions in table format') { |v| options[:display] = v }
|
100
|
+
opts.on('-o', '--output FILE', 'Output CSV file name') { |v| options[:output] = v }
|
101
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
102
|
+
puts opts
|
103
|
+
exit
|
104
|
+
end
|
105
|
+
end.parse!(args)
|
106
|
+
|
107
|
+
# Implement filtering of processed transactions
|
108
|
+
puts "Filtering transactions with options: #{options}"
|
109
|
+
end
|
110
|
+
|
111
|
+
def print_help
|
112
|
+
puts 'Usage: bank_reconciliation.rb [command] [options]'
|
113
|
+
puts 'Commands:'
|
114
|
+
puts ' clean Clean and normalize transaction files'
|
115
|
+
puts ' process Process transaction list via chart of accounts lookup'
|
116
|
+
puts ' filter Filter processed transaction list'
|
117
|
+
puts "Run 'bank_reconciliation.rb [command] --help' for more information on a command."
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
Appydave::Tools::Configuration::Config.configure
|
122
|
+
|
123
|
+
BankReconciliationCLI.new.run
|
124
|
+
# BankReconciliationCLI.new.run if __FILE__ == $PROGRAM_NAME
|
@@ -0,0 +1,113 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
5
|
+
|
6
|
+
require 'appydave/tools'
|
7
|
+
|
8
|
+
# Process command line arguments for SubtitleMaster operations
|
9
|
+
class SubtitleMasterCLI
|
10
|
+
def initialize
|
11
|
+
@commands = {
|
12
|
+
'clean' => method(:clean_subtitles),
|
13
|
+
'correct' => method(:correct_subtitles),
|
14
|
+
'split' => method(:split_subtitles),
|
15
|
+
'highlight' => method(:highlight_subtitles),
|
16
|
+
'image_prompts' => method(:generate_image_prompts)
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def run
|
21
|
+
command, *args = ARGV
|
22
|
+
if @commands.key?(command)
|
23
|
+
@commands[command].call(args)
|
24
|
+
else
|
25
|
+
puts "Unknown command: #{command}"
|
26
|
+
print_help
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def clean_subtitles(args)
|
33
|
+
options = parse_options(args, 'clean')
|
34
|
+
cleaner = Appydave::Tools::SubtitleMaster::Clean.new(options[:file])
|
35
|
+
result = cleaner.clean
|
36
|
+
write_output(result, options[:output])
|
37
|
+
end
|
38
|
+
|
39
|
+
def correct_subtitles(args)
|
40
|
+
options = parse_options(args, 'correct')
|
41
|
+
corrector = Appydave::Tools::SubtitleMaster::Correct.new(options[:file])
|
42
|
+
result = corrector.correct
|
43
|
+
write_output(result, options[:output])
|
44
|
+
end
|
45
|
+
|
46
|
+
def split_subtitles(args)
|
47
|
+
options = parse_options(args, 'split', %i[words_per_group])
|
48
|
+
splitter = Appydave::Tools::SubtitleMaster::Split.new(options[:file], options[:words_per_group])
|
49
|
+
result = splitter.split
|
50
|
+
write_output(result, options[:output])
|
51
|
+
end
|
52
|
+
|
53
|
+
def highlight_subtitles(args)
|
54
|
+
options = parse_options(args, 'highlight')
|
55
|
+
highlighter = Appydave::Tools::SubtitleMaster::Highlight.new(options[:file])
|
56
|
+
result = highlighter.highlight
|
57
|
+
write_output(result, options[:output])
|
58
|
+
end
|
59
|
+
|
60
|
+
def generate_image_prompts(args)
|
61
|
+
options = parse_options(args, 'image_prompts')
|
62
|
+
image_prompter = Appydave::Tools::SubtitleMaster::ImagePrompts.new(options[:file])
|
63
|
+
result = image_prompter.generate_prompts
|
64
|
+
write_output(result, options[:output])
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse_options(args, command, extra_options = [])
|
68
|
+
options = { file: nil, output: nil }
|
69
|
+
OptionParser.new do |opts|
|
70
|
+
opts.banner = "Usage: subtitle_master.rb #{command} [options]"
|
71
|
+
|
72
|
+
opts.on('-f', '--file FILE', 'SRT file to process') { |v| options[:file] = v }
|
73
|
+
opts.on('-o', '--output FILE', 'Output file') { |v| options[:output] = v }
|
74
|
+
|
75
|
+
extra_options.each do |opt|
|
76
|
+
case opt
|
77
|
+
when :words_per_group
|
78
|
+
opts.on('-w', '--words-per-group WORDS', 'Number of words per group for splitting') { |v| options[:words_per_group] = v.to_i }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
83
|
+
puts opts
|
84
|
+
exit
|
85
|
+
end
|
86
|
+
end.parse!(args)
|
87
|
+
|
88
|
+
unless options[:file] && options[:output]
|
89
|
+
puts 'Missing required options. Use -h for help.'
|
90
|
+
exit
|
91
|
+
end
|
92
|
+
|
93
|
+
options
|
94
|
+
end
|
95
|
+
|
96
|
+
def write_output(result, output_file)
|
97
|
+
File.write(output_file, result)
|
98
|
+
puts "Processed file written to #{output_file}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def print_help
|
102
|
+
puts 'Usage: subtitle_master.rb [command] [options]'
|
103
|
+
puts 'Commands:'
|
104
|
+
puts ' clean Clean and normalize SRT files'
|
105
|
+
puts ' correct Correct common typos and mistranslations in SRT files'
|
106
|
+
puts ' split Split subtitle groups based on word count'
|
107
|
+
puts ' highlight Highlight power words in subtitles'
|
108
|
+
puts ' image_prompts Generate image prompts from subtitle text'
|
109
|
+
puts "Run 'subtitle_master.rb [command] --help' for more information on a command."
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
SubtitleMasterCLI.new.run
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Bank reconciliation
|
2
|
+
|
3
|
+
[ChatGPT conversation](https://chatgpt.com/c/5d382562-95e5-4243-9b74-c3807d363486)
|
4
|
+
|
5
|
+
|
6
|
+
## Code structure
|
7
|
+
|
8
|
+
```bash
|
9
|
+
├─ lib
|
10
|
+
│ ├─ appydave
|
11
|
+
│ │ └─ tools
|
12
|
+
│ │ ├─ bank_reconciliation
|
13
|
+
│ │ │ ├─ clean
|
14
|
+
│ │ │ │ ├─ read_transactions.rb
|
15
|
+
│ │ │ │ ├─ transaction_cleaner.rb
|
16
|
+
│ │ │ ├─ models
|
17
|
+
│ │ │ │ ├─ raw_transaction.rb
|
18
|
+
│ │ │ │ └─ reconciled_transaction.rb
|
19
|
+
│ │ └─ configuration
|
20
|
+
│ │ └─ models
|
21
|
+
│ │ └─ bank_reconciliation_config.rb
|
22
|
+
└─ spec
|
23
|
+
├─ appydave
|
24
|
+
│ ├─ tools
|
25
|
+
│ │ ├─ bank_reconciliation
|
26
|
+
│ │ │ ├─ clean
|
27
|
+
│ │ │ │ ├─ read_transactions_spec.rb
|
28
|
+
│ │ │ ├─ models
|
29
|
+
│ │ │ │ └─ raw_transaction_spec.rb
|
30
|
+
│ │ └─ configuration
|
31
|
+
│ │ └─ models
|
32
|
+
│ │ └─ bank_reconciliation_config_spec.rb
|
33
|
+
└─ fixtures
|
34
|
+
└─ bank-reconciliation
|
35
|
+
└─ bank-west.csv
|
36
|
+
```
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Appydave
|
4
|
+
module Tools
|
5
|
+
module BankReconciliation
|
6
|
+
module Clean
|
7
|
+
# Clean transactions
|
8
|
+
class CleanTransactions
|
9
|
+
include Appydave::Tools::Configuration::Configurable
|
10
|
+
include KLog::Logging
|
11
|
+
|
12
|
+
attr_reader :transaction_folder
|
13
|
+
attr_reader :output_folder
|
14
|
+
attr_reader :transactions
|
15
|
+
|
16
|
+
# (config_file)
|
17
|
+
def initialize(transaction_folder: nil, output_folder: nil)
|
18
|
+
# needs to use config.bank_reconciliation.transaction_folder
|
19
|
+
transaction_folder ||= '/Volumes/Expansion/Sync/bank-reconciliation/original-transactions'
|
20
|
+
output_folder ||= File.join(transaction_folder, 'clean')
|
21
|
+
|
22
|
+
@transaction_folder = transaction_folder
|
23
|
+
@output_folder = output_folder
|
24
|
+
end
|
25
|
+
|
26
|
+
def clean_transactions(input_globs, output_file)
|
27
|
+
raw_transactions = grab_raw_transactions(input_globs)
|
28
|
+
transactions, duplicates_count = deduplicate(raw_transactions)
|
29
|
+
|
30
|
+
transactions = Mapper.new.map(transactions)
|
31
|
+
|
32
|
+
# tp transactions, Appydave::Tools::BankReconciliation::Models::Transaction.csv_headers
|
33
|
+
|
34
|
+
log.kv 'Deduped consolidated transactions', duplicates_count if duplicates_count.positive?
|
35
|
+
|
36
|
+
save_to_csv(transactions, output_file)
|
37
|
+
|
38
|
+
@transactions = transactions
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def grab_raw_transactions(input_globs)
|
44
|
+
original_dir = Dir.pwd
|
45
|
+
transactions = []
|
46
|
+
|
47
|
+
begin
|
48
|
+
Dir.chdir(transaction_folder)
|
49
|
+
|
50
|
+
input_globs.each do |glob|
|
51
|
+
Dir.glob(glob).each do |file|
|
52
|
+
log.kv 'Reading transactions from', file
|
53
|
+
raw_transactions = ReadTransactions.new(file).read
|
54
|
+
deduped_transactions, duplicates_count = deduplicate(raw_transactions)
|
55
|
+
|
56
|
+
if duplicates_count.positive?
|
57
|
+
log.kv 'Duplicates count', duplicates_count
|
58
|
+
log.kv 'File', file
|
59
|
+
end
|
60
|
+
|
61
|
+
transactions += deduped_transactions
|
62
|
+
end
|
63
|
+
end
|
64
|
+
ensure
|
65
|
+
Dir.chdir(original_dir)
|
66
|
+
end
|
67
|
+
|
68
|
+
transactions
|
69
|
+
end
|
70
|
+
|
71
|
+
def deduplicate(transactions)
|
72
|
+
unique_transactions = transactions.uniq do |transaction|
|
73
|
+
[
|
74
|
+
transaction.bsb_number,
|
75
|
+
transaction.account_number,
|
76
|
+
transaction.transaction_date,
|
77
|
+
transaction.narration,
|
78
|
+
transaction.cheque_number,
|
79
|
+
transaction.debit,
|
80
|
+
transaction.credit,
|
81
|
+
transaction.balance,
|
82
|
+
transaction.transaction_type
|
83
|
+
]
|
84
|
+
end
|
85
|
+
|
86
|
+
duplicates = transactions.size - unique_transactions.size
|
87
|
+
|
88
|
+
[unique_transactions, duplicates]
|
89
|
+
end
|
90
|
+
|
91
|
+
def save_to_csv(transactions, output_file)
|
92
|
+
FileUtils.mkdir_p(output_folder)
|
93
|
+
output_file = File.join(output_folder, output_file)
|
94
|
+
|
95
|
+
CSV.open(output_file, 'w') do |csv|
|
96
|
+
csv << Appydave::Tools::BankReconciliation::Models::Transaction.csv_headers
|
97
|
+
transactions.each do |transaction|
|
98
|
+
csv << transaction.to_csv_row
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Appydave
|
4
|
+
module Tools
|
5
|
+
module BankReconciliation
|
6
|
+
module Clean
|
7
|
+
# Map transactions to chart of accounts and bank accounts
|
8
|
+
class Mapper
|
9
|
+
include Appydave::Tools::Configuration::Configurable
|
10
|
+
|
11
|
+
# "bank_accounts": [
|
12
|
+
# {
|
13
|
+
# "account_number": "5435 6859 0116 7736",
|
14
|
+
# "bsb": "",
|
15
|
+
# "name": "Mastercard",
|
16
|
+
# "platform": "Bankwest"
|
17
|
+
# },
|
18
|
+
# {
|
19
|
+
# "account_number": "303-092",
|
20
|
+
# "bsb": "1361644",
|
21
|
+
# "name": "atcall",
|
22
|
+
# "platform": "Bankwest"
|
23
|
+
# },
|
24
|
+
|
25
|
+
def map(transactions)
|
26
|
+
transactions.map do |original_transaction|
|
27
|
+
transaction = original_transaction.dup
|
28
|
+
|
29
|
+
transaction = map_chart_of_account(transaction)
|
30
|
+
map_bank_account(transaction)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def map_chart_of_account(transaction)
|
37
|
+
equality_match(transaction) ||
|
38
|
+
trigram_match(transaction, 0.9, '90%') ||
|
39
|
+
trigram_match(transaction, 0.8, '80%') ||
|
40
|
+
trigram_match(transaction, 0.7, '70%') ||
|
41
|
+
trigram_match(transaction, 0.6, '60%') ||
|
42
|
+
trigram_match(transaction, 0.5, '50%') ||
|
43
|
+
start_with_match(transaction) ||
|
44
|
+
includes(transaction)
|
45
|
+
transaction
|
46
|
+
end
|
47
|
+
|
48
|
+
def map_bank_account(transaction)
|
49
|
+
bank_account = config.bank_reconciliation.get_bank_account(transaction.account_number, transaction.bsb_number)
|
50
|
+
|
51
|
+
if bank_account
|
52
|
+
transaction.account_name = bank_account.name
|
53
|
+
transaction.platform = bank_account.platform
|
54
|
+
end
|
55
|
+
|
56
|
+
transaction
|
57
|
+
end
|
58
|
+
|
59
|
+
def equality_match(transaction)
|
60
|
+
coa = config.bank_reconciliation.chart_of_accounts.find do |chart_of_account|
|
61
|
+
chart_of_account.narration.to_s.delete(' ').downcase == transaction.narration.delete(' ').downcase
|
62
|
+
end
|
63
|
+
|
64
|
+
return nil unless coa
|
65
|
+
|
66
|
+
transaction.coa_match_type = 'equality'
|
67
|
+
transaction.coa_code = coa.code
|
68
|
+
transaction
|
69
|
+
end
|
70
|
+
|
71
|
+
def start_with_match(transaction)
|
72
|
+
coa = config.bank_reconciliation.chart_of_accounts.find do |chart_of_account|
|
73
|
+
transaction.narration.to_s.delete(' ').downcase.start_with?(chart_of_account.narration.to_s.downcase)
|
74
|
+
end
|
75
|
+
|
76
|
+
return nil unless coa
|
77
|
+
|
78
|
+
transaction.coa_match_type = 'starts_with'
|
79
|
+
transaction.coa_code = coa.code
|
80
|
+
transaction
|
81
|
+
end
|
82
|
+
|
83
|
+
def includes(transaction)
|
84
|
+
coa = config.bank_reconciliation.chart_of_accounts.find do |chart_of_account|
|
85
|
+
transaction.narration.to_s.delete(' ').downcase.include?(chart_of_account.narration.delete(' ').to_s.downcase)
|
86
|
+
end
|
87
|
+
|
88
|
+
return nil unless coa
|
89
|
+
|
90
|
+
transaction.coa_match_type = 'includes'
|
91
|
+
transaction.coa_code = coa.code
|
92
|
+
transaction
|
93
|
+
end
|
94
|
+
|
95
|
+
def trigram_match(transaction, score_threshold, match_type)
|
96
|
+
scored_transactions = config.bank_reconciliation.chart_of_accounts.map do |coa|
|
97
|
+
{
|
98
|
+
coa: coa,
|
99
|
+
score: compare(coa.narration, transaction.narration)
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
scored_transactions.sort_by! { |t| t[:score] }.reverse!
|
104
|
+
|
105
|
+
best = scored_transactions.first
|
106
|
+
|
107
|
+
return nil unless best
|
108
|
+
return nil if best[:score] < score_threshold
|
109
|
+
|
110
|
+
coa = best[:coa]
|
111
|
+
|
112
|
+
transaction.coa_match_type = match_type
|
113
|
+
transaction.coa_code = coa.code
|
114
|
+
transaction
|
115
|
+
end
|
116
|
+
|
117
|
+
def compare(text1, text2)
|
118
|
+
text1_trigs = trigramify(text1)
|
119
|
+
text2_trigs = trigramify(text2)
|
120
|
+
|
121
|
+
all_cnt = (text1_trigs | text2_trigs).size
|
122
|
+
same_cnt = (text1_trigs & text2_trigs).size
|
123
|
+
|
124
|
+
same_cnt.to_f / all_cnt
|
125
|
+
end
|
126
|
+
|
127
|
+
def trigramify(text)
|
128
|
+
trigs = []
|
129
|
+
text.chars.each_cons(3) { |v| trigs << v.join }
|
130
|
+
trigs
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Appydave
|
4
|
+
module Tools
|
5
|
+
module BankReconciliation
|
6
|
+
module Clean
|
7
|
+
# Read transactions from a CSV file
|
8
|
+
class ReadTransactions
|
9
|
+
attr_reader :platform
|
10
|
+
attr_reader :transactions
|
11
|
+
|
12
|
+
def initialize(file)
|
13
|
+
@file = file
|
14
|
+
end
|
15
|
+
|
16
|
+
def read
|
17
|
+
csv_lines = File.read(@file).lines
|
18
|
+
|
19
|
+
@platform = detect_platform(csv_lines)
|
20
|
+
|
21
|
+
case platform
|
22
|
+
when :bankwest
|
23
|
+
read_bankwest(csv_lines)
|
24
|
+
when :bankwest2
|
25
|
+
read_bankwest2(csv_lines)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def read_bankwest(csv_lines)
|
32
|
+
@transactions = []
|
33
|
+
|
34
|
+
# Skip the header line and parse each subsequent line
|
35
|
+
CSV.parse(csv_lines.join, headers: true).each do |row|
|
36
|
+
transaction = Models::Transaction.new(
|
37
|
+
bsb_number: row['BSB Number'],
|
38
|
+
account_number: row['Account Number'],
|
39
|
+
transaction_date: row['Transaction Date'],
|
40
|
+
narration: row['Narration'],
|
41
|
+
cheque_number: row['Cheque Number'],
|
42
|
+
debit: row['Debit'],
|
43
|
+
credit: row['Credit'],
|
44
|
+
balance: row['Balance'],
|
45
|
+
transaction_type: row['Transaction Type']
|
46
|
+
)
|
47
|
+
@transactions << transaction
|
48
|
+
end
|
49
|
+
|
50
|
+
@transactions
|
51
|
+
end
|
52
|
+
|
53
|
+
def read_bankwest2(csv_lines)
|
54
|
+
@transactions = []
|
55
|
+
|
56
|
+
# Skip the header line and parse each subsequent line
|
57
|
+
CSV.parse(csv_lines.join, headers: true).each do |row|
|
58
|
+
transaction = Models::Transaction.new(
|
59
|
+
bsb_number: row['BSB / Account Number'].split(' - ').first,
|
60
|
+
account_number: row['BSB / Account Number'].split(' - ').last,
|
61
|
+
transaction_date: row['Transaction Date'],
|
62
|
+
narration: row['Narration'],
|
63
|
+
cheque_number: row['Cheque Number'],
|
64
|
+
debit: row['Debit'],
|
65
|
+
credit: row['Credit'],
|
66
|
+
balance: row['Balance'],
|
67
|
+
transaction_type: row['Transaction Type']
|
68
|
+
)
|
69
|
+
@transactions << transaction
|
70
|
+
end
|
71
|
+
|
72
|
+
@transactions
|
73
|
+
end
|
74
|
+
|
75
|
+
# For bankwest the first row is the CSV will look like:
|
76
|
+
# BSB Number,Account Number,Transaction Date,Narration,Cheque Number,Debit,Credit,Balance,Transaction Type
|
77
|
+
def detect_platform(csv_lines)
|
78
|
+
return :bankwest if csv_lines.first.start_with?('BSB Number,Account Number,Transaction Date,Narration,Cheque Number,Debit,Credit,Balance,Transaction Type')
|
79
|
+
return :bankwest2 if csv_lines.first.start_with?('Account Name,BSB / Account Number,Transaction Date,Narration,Cheque Number,Debit,Credit,Balance,Transaction Type')
|
80
|
+
|
81
|
+
puts "Unknown platform detected. CSV columns are: #{csv_lines.first.strip}"
|
82
|
+
raise Appydave::Tools::Error, 'Unknown platform'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Appydave
|
4
|
+
module Tools
|
5
|
+
module BankReconciliation
|
6
|
+
module Models
|
7
|
+
# Unified transaction model for raw and reconciled data
|
8
|
+
class Transaction
|
9
|
+
attr_accessor :bsb_number,
|
10
|
+
:account_number,
|
11
|
+
:transaction_date,
|
12
|
+
:narration,
|
13
|
+
:cheque_number,
|
14
|
+
:debit,
|
15
|
+
:credit,
|
16
|
+
:balance,
|
17
|
+
:transaction_type,
|
18
|
+
:platform,
|
19
|
+
:coa_code,
|
20
|
+
:coa_match_type,
|
21
|
+
:account_name
|
22
|
+
|
23
|
+
def initialize(bsb_number: nil,
|
24
|
+
account_number: nil,
|
25
|
+
transaction_date: nil,
|
26
|
+
narration: nil,
|
27
|
+
cheque_number: nil,
|
28
|
+
debit: nil,
|
29
|
+
credit: nil,
|
30
|
+
balance: nil,
|
31
|
+
transaction_type: nil,
|
32
|
+
platform: nil,
|
33
|
+
coa_code: nil,
|
34
|
+
coa_match_type: nil,
|
35
|
+
account_name: nil)
|
36
|
+
@bsb_number = bsb_number&.strip
|
37
|
+
@account_number = account_number&.strip
|
38
|
+
@transaction_date = transaction_date&.strip
|
39
|
+
@transaction_date = Date.strptime(@transaction_date, '%d/%m/%Y')
|
40
|
+
@narration = narration&.gsub(/\s{2,}/, ' ')&.strip
|
41
|
+
@cheque_number = cheque_number&.strip
|
42
|
+
@debit = debit&.strip
|
43
|
+
@credit = credit&.strip
|
44
|
+
@balance = balance&.strip
|
45
|
+
@transaction_type = transaction_type&.strip
|
46
|
+
@platform = platform
|
47
|
+
@coa_code = coa_code
|
48
|
+
@coa_match_type = coa_match_type
|
49
|
+
@account_name = account_name
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.csv_headers
|
53
|
+
%i[
|
54
|
+
bsb_number
|
55
|
+
account_number
|
56
|
+
transaction_date
|
57
|
+
narration
|
58
|
+
cheque_number
|
59
|
+
debit
|
60
|
+
credit
|
61
|
+
balance
|
62
|
+
transaction_type
|
63
|
+
platform
|
64
|
+
coa_code
|
65
|
+
coa_match_type
|
66
|
+
account_name
|
67
|
+
]
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_csv_row
|
71
|
+
[
|
72
|
+
@bsb_number,
|
73
|
+
@account_number,
|
74
|
+
@transaction_date,
|
75
|
+
@narration,
|
76
|
+
@cheque_number,
|
77
|
+
@debit,
|
78
|
+
@credit,
|
79
|
+
@balance,
|
80
|
+
@transaction_type,
|
81
|
+
@platform,
|
82
|
+
@coa_code,
|
83
|
+
@coa_match_type,
|
84
|
+
@account_name
|
85
|
+
]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -6,6 +6,7 @@ module Appydave
|
|
6
6
|
module Models
|
7
7
|
# Bank reconciliation configuration
|
8
8
|
class BankReconciliationConfig < ConfigBase
|
9
|
+
# def
|
9
10
|
# Retrieve all bank accounts
|
10
11
|
def bank_accounts
|
11
12
|
data['bank_accounts'].map do |account|
|
@@ -54,13 +55,13 @@ module Appydave
|
|
54
55
|
|
55
56
|
# Inner class to represent a bank account
|
56
57
|
class BankAccount
|
57
|
-
attr_accessor :account_number, :bsb, :name, :
|
58
|
+
attr_accessor :account_number, :bsb, :name, :platform
|
58
59
|
|
59
60
|
def initialize(data)
|
60
61
|
@account_number = data['account_number']
|
61
62
|
@bsb = data['bsb']
|
62
63
|
@name = data['name']
|
63
|
-
@
|
64
|
+
@platform = data['platform']
|
64
65
|
end
|
65
66
|
|
66
67
|
def to_h
|
@@ -68,7 +69,7 @@ module Appydave
|
|
68
69
|
'account_number' => @account_number,
|
69
70
|
'bsb' => @bsb,
|
70
71
|
'name' => @name,
|
71
|
-
'
|
72
|
+
'platform' => @platform
|
72
73
|
}
|
73
74
|
end
|
74
75
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Appydave
|
4
|
+
module Tools
|
5
|
+
module SubtitleMaster
|
6
|
+
# Clean and normalize subtitles
|
7
|
+
class Clean
|
8
|
+
def initialize(file_path)
|
9
|
+
@file_path = file_path
|
10
|
+
end
|
11
|
+
|
12
|
+
def clean
|
13
|
+
content = File.read(@file_path, encoding: 'UTF-8')
|
14
|
+
content = remove_underscores(content)
|
15
|
+
normalize_lines(content)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def remove_underscores(content)
|
21
|
+
content.gsub(%r{</?u>}, '')
|
22
|
+
end
|
23
|
+
|
24
|
+
def normalize_lines(content)
|
25
|
+
lines = content.split("\n")
|
26
|
+
grouped_subtitles = []
|
27
|
+
current_subtitle = { text: '', start_time: nil, end_time: nil }
|
28
|
+
|
29
|
+
lines.each do |line|
|
30
|
+
if line =~ /^\d+$/ || line.strip.empty?
|
31
|
+
next
|
32
|
+
elsif line =~ /^\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}$/
|
33
|
+
if current_subtitle[:start_time]
|
34
|
+
grouped_subtitles << current_subtitle.clone
|
35
|
+
current_subtitle = { text: '', start_time: nil, end_time: nil }
|
36
|
+
end
|
37
|
+
|
38
|
+
times = line.split(' --> ')
|
39
|
+
current_subtitle[:start_time] = times[0]
|
40
|
+
current_subtitle[:end_time] = times[1]
|
41
|
+
else
|
42
|
+
current_subtitle[:text] += ' ' unless current_subtitle[:text].empty?
|
43
|
+
current_subtitle[:text] += line.strip
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
grouped_subtitles << current_subtitle unless current_subtitle[:text].empty?
|
48
|
+
|
49
|
+
grouped_subtitles = merge_subtitles(grouped_subtitles)
|
50
|
+
|
51
|
+
build_normalized_content(grouped_subtitles)
|
52
|
+
end
|
53
|
+
|
54
|
+
def merge_subtitles(subtitles)
|
55
|
+
merged_subtitles = []
|
56
|
+
subtitles.each do |subtitle|
|
57
|
+
if merged_subtitles.empty? || merged_subtitles.last[:text] != subtitle[:text]
|
58
|
+
merged_subtitles << subtitle
|
59
|
+
else
|
60
|
+
merged_subtitles.last[:end_time] = subtitle[:end_time]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
merged_subtitles
|
64
|
+
end
|
65
|
+
|
66
|
+
def build_normalized_content(grouped_subtitles)
|
67
|
+
normalized_content = []
|
68
|
+
grouped_subtitles.each_with_index do |subtitle, index|
|
69
|
+
normalized_content << (index + 1).to_s
|
70
|
+
normalized_content << "#{subtitle[:start_time]} --> #{subtitle[:end_time]}"
|
71
|
+
normalized_content << subtitle[:text]
|
72
|
+
normalized_content << ''
|
73
|
+
end
|
74
|
+
|
75
|
+
normalized_content.join("\n")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/appydave/tools.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'clipboard'
|
4
|
+
require 'csv'
|
4
5
|
require 'fileutils'
|
5
6
|
require 'json'
|
6
7
|
require 'open3'
|
7
8
|
require 'openai'
|
8
9
|
require 'optparse'
|
9
10
|
require 'k_log'
|
11
|
+
require 'pry'
|
10
12
|
|
11
13
|
require 'appydave/tools/version'
|
12
14
|
require 'appydave/tools/gpt_context/file_collector'
|
@@ -19,6 +21,12 @@ require 'appydave/tools/configuration/models/settings_config'
|
|
19
21
|
require 'appydave/tools/configuration/models/bank_reconciliation_config'
|
20
22
|
require 'appydave/tools/configuration/models/channels_config'
|
21
23
|
require 'appydave/tools/name_manager/project_name'
|
24
|
+
require 'appydave/tools/bank_reconciliation/clean/clean_transactions'
|
25
|
+
require 'appydave/tools/bank_reconciliation/clean/read_transactions'
|
26
|
+
require 'appydave/tools/bank_reconciliation/clean/mapper'
|
27
|
+
require 'appydave/tools/bank_reconciliation/models/transaction'
|
28
|
+
|
29
|
+
require 'appydave/tools/subtitle_master/clean'
|
22
30
|
|
23
31
|
Appydave::Tools::Configuration::Config.set_default do |config|
|
24
32
|
config.config_path = File.expand_path('~/.config/appydave')
|
data/package-lock.json
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
{
|
2
2
|
"name": "appydave-tools",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.8.0",
|
4
4
|
"lockfileVersion": 3,
|
5
5
|
"requires": true,
|
6
6
|
"packages": {
|
7
7
|
"": {
|
8
8
|
"name": "appydave-tools",
|
9
|
-
"version": "0.
|
9
|
+
"version": "0.8.0",
|
10
10
|
"devDependencies": {
|
11
11
|
"@klueless-js/semantic-release-rubygem": "github:klueless-js/semantic-release-rubygem",
|
12
12
|
"@semantic-release/changelog": "^6.0.3",
|
data/package.json
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: appydave-tools
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Cruwys
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-06-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: clipboard
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: csv
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: dotenv
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -86,12 +100,19 @@ files:
|
|
86
100
|
- LICENSE.txt
|
87
101
|
- README.md
|
88
102
|
- Rakefile
|
103
|
+
- bin/bank_reconciliation.rb
|
89
104
|
- bin/configuration.rb
|
90
105
|
- bin/console
|
91
106
|
- bin/gpt_context.rb
|
92
107
|
- bin/setup
|
108
|
+
- bin/subtitle_master.rb
|
93
109
|
- images.log
|
94
110
|
- lib/appydave/tools.rb
|
111
|
+
- lib/appydave/tools/bank_reconciliation/_doc.md
|
112
|
+
- lib/appydave/tools/bank_reconciliation/clean/clean_transactions.rb
|
113
|
+
- lib/appydave/tools/bank_reconciliation/clean/mapper.rb
|
114
|
+
- lib/appydave/tools/bank_reconciliation/clean/read_transactions.rb
|
115
|
+
- lib/appydave/tools/bank_reconciliation/models/transaction.rb
|
95
116
|
- lib/appydave/tools/configuration/_doc.md
|
96
117
|
- lib/appydave/tools/configuration/config.rb
|
97
118
|
- lib/appydave/tools/configuration/configurable.rb
|
@@ -105,6 +126,8 @@ files:
|
|
105
126
|
- lib/appydave/tools/gpt_context/file_collector.rb
|
106
127
|
- lib/appydave/tools/name_manager/_doc.md
|
107
128
|
- lib/appydave/tools/name_manager/project_name.rb
|
129
|
+
- lib/appydave/tools/subtitle_master/_doc.md
|
130
|
+
- lib/appydave/tools/subtitle_master/clean.rb
|
108
131
|
- lib/appydave/tools/version.rb
|
109
132
|
- package-lock.json
|
110
133
|
- package.json
|