baa_chan 0.0.1
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 +7 -0
- data/bin/baa_chan +5 -0
- data/lib/baa_chan.rb +3 -0
- data/lib/baa_chan/costs.rb +18 -0
- data/lib/baa_chan/layout.rb +31 -0
- data/lib/baa_chan/layouts/clear.yml +30 -0
- data/lib/baa_chan/layouts/genial.yml +36 -0
- data/lib/baa_chan/layouts/singulare.yml +36 -0
- data/lib/baa_chan/parser.rb +136 -0
- data/lib/baa_chan/pdf_to_text.rb +21 -0
- data/lib/baa_chan/reader.rb +55 -0
- data/lib/baa_chan/trade.rb +14 -0
- data/lib/baa_chan/trade_confirmation.rb +53 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6dd4992c1495048ce9123656452c1d9e2ed8ce3dffd2c7f070172c958f748cca
|
4
|
+
data.tar.gz: b6a55877a52d39e01aeb3bb6f11d54a6f7f9a86e5622b70ba4e82e13e1302701
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 392c751ceccd693dfb2a85d6140b69c8d1bfea4f08088917f8de8e0965deef3234b839aaba431a35ea996041cf0c4a50932b3353039e184cfda1a3b2fd178869
|
7
|
+
data.tar.gz: 62c4a801ea57f5ff746a3edec50458ae18ad4e3ca8d01330bbdbf259ebe3d4eb3dd7132b4e46e654807558c6fe2aafdf77543c7f1d768c9f3d9c7524c96dc5fb
|
data/bin/baa_chan
ADDED
data/lib/baa_chan.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BaaChan
|
4
|
+
class Costs
|
5
|
+
attr_accessor :clearing_fee, :registration_fee, :emoluments,
|
6
|
+
:brokerage, :iss, :irrf, :pis_cofins
|
7
|
+
|
8
|
+
def initialize(brokerage, clearing_fee, registration_fee, emoluments, options)
|
9
|
+
@brokerage = brokerage
|
10
|
+
@clearing_fee = clearing_fee
|
11
|
+
@registration_fee = registration_fee
|
12
|
+
@emoluments = emoluments
|
13
|
+
@iss = options[:iss] || 0.0
|
14
|
+
@irrf = options[:irrf] || 0.0
|
15
|
+
@pis_cofins = options[:pis_cofins] || 0.0
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Layout
|
4
|
+
LAYOUTS_PATH = 'lib/baa_chan/layouts'
|
5
|
+
|
6
|
+
def initialize(broker)
|
7
|
+
@broker = broker
|
8
|
+
end
|
9
|
+
|
10
|
+
def attributes
|
11
|
+
@attributes ||= YAML.safe_load(File.read(File.join(LAYOUTS_PATH, "#{@broker}.yml")))
|
12
|
+
end
|
13
|
+
|
14
|
+
def line
|
15
|
+
attributes[caller_locations.first.label]['line'].to_i
|
16
|
+
end
|
17
|
+
|
18
|
+
def index(attr_name = nil)
|
19
|
+
attr = attr_name || caller_locations.first.label
|
20
|
+
|
21
|
+
attributes[attr]['index'].to_i
|
22
|
+
end
|
23
|
+
|
24
|
+
def trade_prefix
|
25
|
+
attributes['trades']['prefix']
|
26
|
+
end
|
27
|
+
|
28
|
+
def regexp_for(attr)
|
29
|
+
attributes[attr]['regexp']
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
broker:
|
2
|
+
line: 4
|
3
|
+
trade_confirmation_number:
|
4
|
+
line: 2
|
5
|
+
index: 0
|
6
|
+
trade_date:
|
7
|
+
line: 2
|
8
|
+
index: 2
|
9
|
+
trades:
|
10
|
+
prefix: 1-BOVESPA
|
11
|
+
operation:
|
12
|
+
index: 1
|
13
|
+
ticker:
|
14
|
+
index: 3
|
15
|
+
quantity:
|
16
|
+
index: 7
|
17
|
+
price:
|
18
|
+
index: 8
|
19
|
+
clearing_fee:
|
20
|
+
regexp: .*Taxa de liquidação.*
|
21
|
+
index: 7
|
22
|
+
registration_fee:
|
23
|
+
regexp: .*Taxa de Registro.*
|
24
|
+
index: 7
|
25
|
+
emoluments:
|
26
|
+
regexp: .*Emolumentos.*
|
27
|
+
index: 1
|
28
|
+
irrf:
|
29
|
+
regexp: .*I\.R\.R\.F.*
|
30
|
+
index: 5
|
@@ -0,0 +1,36 @@
|
|
1
|
+
broker:
|
2
|
+
line: 4
|
3
|
+
trade_confirmation_number:
|
4
|
+
line: 2
|
5
|
+
index: 0
|
6
|
+
trade_date:
|
7
|
+
line: 2
|
8
|
+
index: 2
|
9
|
+
trades:
|
10
|
+
prefix: 1-BOVESPA
|
11
|
+
operation:
|
12
|
+
index: 1
|
13
|
+
ticker:
|
14
|
+
index: 3
|
15
|
+
quantity:
|
16
|
+
index: 6
|
17
|
+
price:
|
18
|
+
index: 7
|
19
|
+
clearing_fee:
|
20
|
+
regexp: .*Taxa de liquidação.*
|
21
|
+
index: 7
|
22
|
+
registration_fee:
|
23
|
+
regexp: .*Taxa de Registro.*
|
24
|
+
index: 7
|
25
|
+
emoluments:
|
26
|
+
regexp: .*Emolumentos.*
|
27
|
+
index: 1
|
28
|
+
brokerage:
|
29
|
+
regexp: ^A coluna Q.*
|
30
|
+
index: 9
|
31
|
+
iss:
|
32
|
+
regexp: ^ISS \( SÃO PAULO \).*
|
33
|
+
index: 5
|
34
|
+
pis_cofins:
|
35
|
+
regexp: ^ISS/PIS/COFINS.*
|
36
|
+
index: 1
|
@@ -0,0 +1,36 @@
|
|
1
|
+
broker:
|
2
|
+
line: 3
|
3
|
+
trade_confirmation_number:
|
4
|
+
line: 2
|
5
|
+
index: 0
|
6
|
+
trade_date:
|
7
|
+
line: 2
|
8
|
+
index: 2
|
9
|
+
trades:
|
10
|
+
prefix: BM&FBOVESPA S/A.
|
11
|
+
operation:
|
12
|
+
index: 2
|
13
|
+
ticker:
|
14
|
+
index: 6
|
15
|
+
quantity:
|
16
|
+
index: 9
|
17
|
+
price:
|
18
|
+
index: 10
|
19
|
+
clearing_fee:
|
20
|
+
regexp: .*Taxa de Liquidação.*
|
21
|
+
index: 7
|
22
|
+
registration_fee:
|
23
|
+
regexp: .*Taxa de Registro.*
|
24
|
+
index: 7
|
25
|
+
emoluments:
|
26
|
+
regexp: .*Emolumentos.*
|
27
|
+
index: 5
|
28
|
+
brokerage:
|
29
|
+
regexp: ^Corretagem.*
|
30
|
+
index: 1
|
31
|
+
iss:
|
32
|
+
regexp: ^ISS \(SAO PAULO\).*
|
33
|
+
index: 3
|
34
|
+
irrf:
|
35
|
+
regexp: .*I\.R\.R\.F.*
|
36
|
+
index: 14
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'baa_chan/trade_confirmation'
|
4
|
+
require 'baa_chan/trade'
|
5
|
+
require 'baa_chan/costs'
|
6
|
+
require 'date'
|
7
|
+
require 'yaml'
|
8
|
+
|
9
|
+
module BaaChan
|
10
|
+
class CostsParserError < StandardError; end
|
11
|
+
|
12
|
+
class Parser
|
13
|
+
def initialize(lines, layout)
|
14
|
+
@lines = lines
|
15
|
+
@layout = layout
|
16
|
+
end
|
17
|
+
|
18
|
+
def call
|
19
|
+
TradeConfirmation.new(broker, trade_confirmation_number, trade_date, trades, costs)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def broker
|
25
|
+
@broker ||= @lines[@layout.line]
|
26
|
+
end
|
27
|
+
|
28
|
+
def trade_confirmation_number
|
29
|
+
@trade_confirmation_number ||= @lines[@layout.line].split[@layout.index]
|
30
|
+
end
|
31
|
+
|
32
|
+
def trade_date
|
33
|
+
@trade_date ||= Date.parse @lines[@layout.line].split[@layout.index]
|
34
|
+
end
|
35
|
+
|
36
|
+
def trades
|
37
|
+
@trades ||= TradeParser.new(@lines, @layout).parse
|
38
|
+
end
|
39
|
+
|
40
|
+
def costs
|
41
|
+
@costs ||= CostsParser.new(@lines, @layout).parse
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class TradeParser
|
46
|
+
def initialize(lines, layout)
|
47
|
+
@lines = lines
|
48
|
+
@layout = layout
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse
|
52
|
+
@lines.each_with_object([]) do |line, trades|
|
53
|
+
next unless line.include? @layout.trade_prefix
|
54
|
+
|
55
|
+
@trade_line = line
|
56
|
+
trades << Trade.new(operation, ticker, quantity, price)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def operation
|
63
|
+
@trade_line.split[@layout.index] == 'V' ? 'Sell' : 'Buy'
|
64
|
+
end
|
65
|
+
|
66
|
+
def ticker
|
67
|
+
@trade_line.split[@layout.index]
|
68
|
+
end
|
69
|
+
|
70
|
+
def quantity
|
71
|
+
@trade_line.split[@layout.index].to_i
|
72
|
+
end
|
73
|
+
|
74
|
+
def price
|
75
|
+
@trade_line.split[@layout.index].gsub(',', '.').to_f
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class CostsParser
|
80
|
+
def initialize(lines, layout)
|
81
|
+
@lines = lines
|
82
|
+
@layout = layout
|
83
|
+
end
|
84
|
+
|
85
|
+
def parse
|
86
|
+
Costs.new(brokerage, clearing_fee, registration_fee, emoluments, { iss: iss, irrf: irrf, pis_cofins: pis_cofins })
|
87
|
+
rescue StandardError => e
|
88
|
+
raise CostsParserError, e.message
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def value_for(attr_name)
|
94
|
+
@lines.find { |line| line.match? @layout.regexp_for(attr_name) }
|
95
|
+
.split[@layout.index(attr_name)]
|
96
|
+
.gsub(',', '.')
|
97
|
+
.to_f
|
98
|
+
end
|
99
|
+
|
100
|
+
def brokerage
|
101
|
+
value_for('brokerage')
|
102
|
+
rescue NoMethodError
|
103
|
+
0.0
|
104
|
+
end
|
105
|
+
|
106
|
+
def clearing_fee
|
107
|
+
value_for('clearing_fee')
|
108
|
+
end
|
109
|
+
|
110
|
+
def registration_fee
|
111
|
+
value_for('registration_fee')
|
112
|
+
end
|
113
|
+
|
114
|
+
def emoluments
|
115
|
+
value_for('emoluments')
|
116
|
+
end
|
117
|
+
|
118
|
+
def iss
|
119
|
+
value_for('iss')
|
120
|
+
rescue NoMethodError
|
121
|
+
0.0
|
122
|
+
end
|
123
|
+
|
124
|
+
def irrf
|
125
|
+
value_for('irrf')
|
126
|
+
rescue NoMethodError
|
127
|
+
0.0
|
128
|
+
end
|
129
|
+
|
130
|
+
def pis_cofins
|
131
|
+
value_for('pis_cofins')
|
132
|
+
rescue NoMethodError
|
133
|
+
0.0
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'English'
|
4
|
+
|
5
|
+
module BaaChan
|
6
|
+
class UnreadablePDFError < StandardError; end
|
7
|
+
|
8
|
+
class InvalidExtensionError < StandardError; end
|
9
|
+
|
10
|
+
class PdfToText
|
11
|
+
def self.call(source)
|
12
|
+
raise InvalidExtensionError unless source.match /\.pdf$/
|
13
|
+
|
14
|
+
content = `pdftotext -layout #{source} -`
|
15
|
+
|
16
|
+
raise UnreadablePDFError unless $CHILD_STATUS.success?
|
17
|
+
|
18
|
+
content
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BaaChan
|
4
|
+
class MalformedPDFError < StandardError; end
|
5
|
+
|
6
|
+
class UnknownLayoutError < StandardError; end
|
7
|
+
|
8
|
+
class Reader
|
9
|
+
BROKER_LIST = {
|
10
|
+
singulare: 'Singulare - corretora de titulos de valores mobiliarios',
|
11
|
+
genial: 'GENIAL INVESTIMENTOS CORRETORA DE VALORES MOBILIÁRIOS S.A.',
|
12
|
+
clear: 'CLEAR CORRETORA - GRUPO XP'
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def initialize(source)
|
16
|
+
@source = source
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
parse(PdfToText.call(@source))
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def parse(text)
|
26
|
+
lines = sanitize(text)
|
27
|
+
|
28
|
+
layout = Layout.new(detect_layout(lines))
|
29
|
+
|
30
|
+
Parser.new(lines, layout).call
|
31
|
+
end
|
32
|
+
|
33
|
+
def sanitize(content)
|
34
|
+
content.split("\n").map(&:strip)
|
35
|
+
end
|
36
|
+
|
37
|
+
def detect_layout(lines)
|
38
|
+
broker = if lines[3] == BROKER_LIST[:singulare]
|
39
|
+
'singulare'
|
40
|
+
elsif lines[4] == BROKER_LIST[:genial]
|
41
|
+
'genial'
|
42
|
+
elsif lines[4] == BROKER_LIST[:clear]
|
43
|
+
'clear'
|
44
|
+
else
|
45
|
+
raise UnknownLayoutError
|
46
|
+
end
|
47
|
+
|
48
|
+
return broker
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
require 'baa_chan/pdf_to_text'
|
54
|
+
require 'baa_chan/parser'
|
55
|
+
require 'baa_chan/layout'
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BaaChan
|
4
|
+
class Trade
|
5
|
+
attr_accessor :operation, :ticker, :quantity, :price
|
6
|
+
|
7
|
+
def initialize(operation, ticker, quantity, price)
|
8
|
+
@operation = operation
|
9
|
+
@ticker = ticker
|
10
|
+
@quantity = quantity
|
11
|
+
@price = price
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jbuilder'
|
4
|
+
|
5
|
+
module BaaChan
|
6
|
+
class TradeConfirmation
|
7
|
+
attr_accessor :broker, :trade_confirmation_number, :trade_date, :trades, :costs
|
8
|
+
|
9
|
+
def initialize(broker, trade_confirmation_number, trade_date, trades, costs)
|
10
|
+
@broker = broker
|
11
|
+
@trade_confirmation_number = trade_confirmation_number
|
12
|
+
@trade_date = trade_date
|
13
|
+
@trades = trades
|
14
|
+
@costs = costs
|
15
|
+
end
|
16
|
+
|
17
|
+
def total_cost
|
18
|
+
@costs.brokerage +
|
19
|
+
@costs.registration_fee +
|
20
|
+
@costs.emoluments +
|
21
|
+
@costs.pis_cofins +
|
22
|
+
@costs.clearing_fee
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_builder
|
26
|
+
Jbuilder.encode do |json|
|
27
|
+
json.call(
|
28
|
+
self,
|
29
|
+
:broker,
|
30
|
+
:trade_confirmation_number,
|
31
|
+
:trade_date
|
32
|
+
)
|
33
|
+
|
34
|
+
json.trades @trades do |trade|
|
35
|
+
json.operation trade.operation
|
36
|
+
json.ticker trade.ticker
|
37
|
+
json.quantity trade.quantity
|
38
|
+
json.price trade.price
|
39
|
+
end
|
40
|
+
|
41
|
+
json.costs do
|
42
|
+
json.brokerage @costs.brokerage
|
43
|
+
json.clearing_fee @costs.clearing_fee
|
44
|
+
json.registration_fee @costs.registration_fee
|
45
|
+
json.emoluments @costs.emoluments
|
46
|
+
json.iss @costs.iss if @costs.iss
|
47
|
+
json.irrf @costs.irrf if @costs.irrf
|
48
|
+
json.pis_cofins @costs.pis_cofins if @costs.pis_cofins
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: baa_chan
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Diogo Noda
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-04-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: pry-byebug
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.9.0
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 3.9.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.10.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.10.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: jbuilder
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.11.2
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.11.2
|
55
|
+
description: |
|
56
|
+
BaaChan will read your trade confirmations and provide you
|
57
|
+
details about your trades helping you out handling your
|
58
|
+
finances
|
59
|
+
email: diogotnoda@gmail.com
|
60
|
+
executables:
|
61
|
+
- baa_chan
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- bin/baa_chan
|
66
|
+
- lib/baa_chan.rb
|
67
|
+
- lib/baa_chan/costs.rb
|
68
|
+
- lib/baa_chan/layout.rb
|
69
|
+
- lib/baa_chan/layouts/clear.yml
|
70
|
+
- lib/baa_chan/layouts/genial.yml
|
71
|
+
- lib/baa_chan/layouts/singulare.yml
|
72
|
+
- lib/baa_chan/parser.rb
|
73
|
+
- lib/baa_chan/pdf_to_text.rb
|
74
|
+
- lib/baa_chan/reader.rb
|
75
|
+
- lib/baa_chan/trade.rb
|
76
|
+
- lib/baa_chan/trade_confirmation.rb
|
77
|
+
homepage: https://rubygems.org/gems/baa_chan
|
78
|
+
licenses:
|
79
|
+
- MIT
|
80
|
+
metadata: {}
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.0.0
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubygems_version: 3.2.3
|
97
|
+
signing_key:
|
98
|
+
specification_version: 4
|
99
|
+
summary: Trade Confirmation Reader
|
100
|
+
test_files: []
|