pennylane 0.1.0 → 1.0.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/CHANGELOG.md +23 -1
- data/Gemfile.lock +22 -1
- data/README.md +188 -2
- data/lib/pennylane/client.rb +8 -8
- data/lib/pennylane/object.rb +3 -8
- data/lib/pennylane/resources/base.rb +47 -12
- data/lib/pennylane/resources/category.rb +27 -0
- data/lib/pennylane/resources/category_group.rb +16 -0
- data/lib/pennylane/resources/customer.rb +3 -2
- data/lib/pennylane/resources/customer_invoice.rb +90 -0
- data/lib/pennylane/resources/product.rb +22 -0
- data/lib/pennylane/resources/supplier.rb +20 -0
- data/lib/pennylane/util.rb +23 -8
- data/lib/pennylane/version.rb +2 -2
- data/lib/pennylane.rb +6 -1
- data/pennylane.gemspec +1 -0
- metadata +21 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1b079bfa7d777a0adb18de2cb8b02c4b5b3f7a20ef14ee3be35ccc56c4fc5714
|
4
|
+
data.tar.gz: 417d611044b5329eb04040ffaab0c1e78e609b6dc5613deeeac35994aab88012
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d1e5ec1c7aacb11921d8d7490c8bfcfd626fb185a74c6199b1ac9b75e4b4cae03dc8f66be2e70effada0cc21a1d563b1aef205beca5d906bc522637d76b429c
|
7
|
+
data.tar.gz: fbec4d19969a69390de3903fcb7a1e9e372fcdc4d6bb9b3f735ac09e3a31b7bbea14963e0e35d0c0815d42140385e168e21f21e764caae06d6fdbea08ebec21c
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,27 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.0.0] - 2024-04-29
|
4
|
+
|
5
|
+
- Support CustomerInvoice:
|
6
|
+
- `finalize`
|
7
|
+
- `mark_as_paid`
|
8
|
+
- `send_by_email`
|
9
|
+
- `links`
|
10
|
+
- `import`
|
11
|
+
- Support per request `api_key` e.g `Pennylane::Customer.retrieve('cus_id', {api_key: 'x0fd....'})`
|
12
|
+
- Endpoints returning empty response we are doing +1 GET request at the resource. Not the best solution but at least we are sure we have fresh data.
|
13
|
+
- Added resource properties access with `#[]` e.g `cus['name']`
|
14
|
+
|
15
|
+
## [0.2.0-alpha] - 2024-04-20
|
16
|
+
|
17
|
+
- Support resources
|
18
|
+
- `CategoryGroup`
|
19
|
+
- `Category`
|
20
|
+
- `Product`
|
21
|
+
- Added missing `#create` to `Supplier`
|
22
|
+
- Adding `#update` to `Customer` and `Supplier`
|
23
|
+
|
24
|
+
|
3
25
|
## [0.1.0] - 2024-04-04
|
4
26
|
|
5
|
-
- Initial release
|
27
|
+
- Initial release
|
data/Gemfile.lock
CHANGED
@@ -1,15 +1,33 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pennylane (
|
4
|
+
pennylane (1.0.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
+
activesupport (7.1.3.2)
|
10
|
+
base64
|
11
|
+
bigdecimal
|
12
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
13
|
+
connection_pool (>= 2.2.5)
|
14
|
+
drb
|
15
|
+
i18n (>= 1.6, < 2)
|
16
|
+
minitest (>= 5.1)
|
17
|
+
mutex_m
|
18
|
+
tzinfo (~> 2.0)
|
9
19
|
ast (2.4.2)
|
20
|
+
base64 (0.2.0)
|
21
|
+
bigdecimal (3.1.7)
|
22
|
+
concurrent-ruby (1.2.3)
|
23
|
+
connection_pool (2.4.1)
|
24
|
+
drb (2.2.1)
|
25
|
+
i18n (1.14.4)
|
26
|
+
concurrent-ruby (~> 1.0)
|
10
27
|
json (2.7.2)
|
11
28
|
language_server-protocol (3.17.0.3)
|
12
29
|
minitest (5.22.3)
|
30
|
+
mutex_m (0.2.0)
|
13
31
|
parallel (1.24.0)
|
14
32
|
parser (3.3.0.5)
|
15
33
|
ast (~> 2.4.1)
|
@@ -36,6 +54,8 @@ GEM
|
|
36
54
|
ruby-progressbar (1.13.0)
|
37
55
|
test-unit (3.6.2)
|
38
56
|
power_assert
|
57
|
+
tzinfo (2.0.6)
|
58
|
+
concurrent-ruby (~> 1.0)
|
39
59
|
unicode-display_width (2.5.0)
|
40
60
|
vcr (6.2.0)
|
41
61
|
|
@@ -43,6 +63,7 @@ PLATFORMS
|
|
43
63
|
arm64-darwin-22
|
44
64
|
|
45
65
|
DEPENDENCIES
|
66
|
+
activesupport (~> 7.1)
|
46
67
|
minitest (~> 5.22)
|
47
68
|
pennylane!
|
48
69
|
rake (~> 13.0)
|
data/README.md
CHANGED
@@ -32,12 +32,177 @@ Pennylane.api_key = 'x0fd....'
|
|
32
32
|
|
33
33
|
# list customers
|
34
34
|
Pennylane::Customer.list
|
35
|
+
```
|
36
|
+
|
37
|
+
### Customers ([API Doc](https://pennylane.readme.io/reference/customers-post-1))
|
35
38
|
|
39
|
+
```ruby
|
36
40
|
# filter and paginate customers
|
37
41
|
Pennylane::Customer.list(filter: [{field: 'name', operator: 'eq', value: 'Apple'}], page: 2)
|
38
42
|
|
39
43
|
# Retrieve single customer
|
40
44
|
Pennylane::Customer.retrieve('38a1f19a-256d-4692-a8fe-0a16403f59ff')
|
45
|
+
|
46
|
+
# Update a customer
|
47
|
+
cus = Pennylane::Customer.retrieve('38a1f19a-256d-4692-a8fe-0a16403f59ff')
|
48
|
+
cus.update(name: 'Apple Inc')
|
49
|
+
|
50
|
+
# Create a customer
|
51
|
+
Pennylane::Customer.create customer_type: 'company', name: 'Tesla', address: '4 rue du faubourg', postal_code: '75008', city: 'Paris', country_alpha2: 'FR'
|
52
|
+
```
|
53
|
+
|
54
|
+
### CustomerInvoices ([API Doc](https://pennylane.readme.io/reference/customer_invoices-import-1))
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
# Create a customer invoice
|
58
|
+
Pennylane::CustomerInvoice.create(
|
59
|
+
create_customer: true,
|
60
|
+
create_products: true,
|
61
|
+
invoice: {
|
62
|
+
date: '2021-01-01',
|
63
|
+
deadline: '2021-01-31',
|
64
|
+
customer: {
|
65
|
+
name: 'Tesla',
|
66
|
+
customer_type: 'company',
|
67
|
+
address: '4 rue du faubourg',
|
68
|
+
postal_code: '75001',
|
69
|
+
city: 'Paris',
|
70
|
+
country_alpha2: 'FR',
|
71
|
+
emails: ['stephane@tesla.com'] },
|
72
|
+
line_items: [
|
73
|
+
{
|
74
|
+
description: 'Consulting',
|
75
|
+
quantity: 1,
|
76
|
+
unit_price: 1000
|
77
|
+
}
|
78
|
+
]
|
79
|
+
}
|
80
|
+
)
|
81
|
+
|
82
|
+
# List customer invoices
|
83
|
+
Pennylane::CustomerInvoice.list
|
84
|
+
|
85
|
+
# Retrieve a customer invoice
|
86
|
+
invoice = Pennylane::CustomerInvoice.retrieve('38a1f19a-256d-4692-a8fe-0a16403f59ff')
|
87
|
+
|
88
|
+
# Finalize a customer invoice
|
89
|
+
invoice.finalize
|
90
|
+
|
91
|
+
# Send a customer invoice
|
92
|
+
invoice.send_by_email
|
93
|
+
|
94
|
+
# Mark a customer invoice as paid
|
95
|
+
invoice.mark_as_paid
|
96
|
+
|
97
|
+
# Link an invoice and a credit note
|
98
|
+
credit_note = Pennylane::CustomerInvoice.retrieve('some-credit-note-id')
|
99
|
+
Pennylane::CustomerInvoice.links(invoice.quote_group_uuid, credit_note.quote_group_uuid)
|
100
|
+
|
101
|
+
# Import a customer invoice
|
102
|
+
Pennylane::CustomerInvoice.import(file: Util.file(File.expand_path('../fixtures/files/invoice.pdf', __FILE__)),
|
103
|
+
create_customer: true,
|
104
|
+
invoice: { date: Date.today, deadline: Date.today >> 1,
|
105
|
+
customer: {
|
106
|
+
name: 'Tesla',
|
107
|
+
customer_type: 'company',
|
108
|
+
address: '4 rue du faubourg',
|
109
|
+
postal_code: '75001',
|
110
|
+
city: 'Paris',
|
111
|
+
country_alpha2: 'FR',
|
112
|
+
emails: ['stephane@tesla.com'] },
|
113
|
+
line_items: [
|
114
|
+
{
|
115
|
+
description: 'Consulting',
|
116
|
+
quantity: 1,
|
117
|
+
unit_price: 1000
|
118
|
+
}
|
119
|
+
]
|
120
|
+
}
|
121
|
+
)
|
122
|
+
```
|
123
|
+
### Suppliers ([API Doc](https://pennylane.readme.io/reference/suppliers-post))
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
# Create a supplier
|
127
|
+
Pennylane::Supplier.create(name: 'Apple Inc', address: '4 rue du faubourg', postal_code: '75008', city: 'Paris', country_alpha2: 'FR')
|
128
|
+
|
129
|
+
# Retrieve a supplier
|
130
|
+
Pennylane::Supplier.retrieve('supplier_id')
|
131
|
+
|
132
|
+
# List all suppliers
|
133
|
+
Pennylane::Supplier.list
|
134
|
+
```
|
135
|
+
|
136
|
+
### Products ([API Doc](https://pennylane.readme.io/reference/products-post-1))
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
# Create a product
|
140
|
+
Pennylane::Product.create(label: 'Macbook Pro', unit: 'piece', price: 2_999, vat_rate: 'FR_200', currency: 'EUR')
|
141
|
+
|
142
|
+
# List all products
|
143
|
+
Pennylane::Product.list
|
144
|
+
|
145
|
+
# Retrieve a product
|
146
|
+
product = Pennylane::Product.retrieve('product_id')
|
147
|
+
|
148
|
+
# Update a product
|
149
|
+
product.update(label: 'Macbook Pro 2021')
|
150
|
+
```
|
151
|
+
|
152
|
+
### Categories ([API Doc](https://pennylane.readme.io/reference/tags-get))
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
# Create a category
|
156
|
+
Pennylane::Category.create(name: 'IT')
|
157
|
+
|
158
|
+
# Retrieve a category
|
159
|
+
Pennylane::Category.retrieve('category_id')
|
160
|
+
|
161
|
+
# List all categories
|
162
|
+
Pennylane::Category.list
|
163
|
+
|
164
|
+
# Update a category
|
165
|
+
category = Pennylane::Category.retrieve('category_id')
|
166
|
+
category.update(name: 'IT Services')
|
167
|
+
```
|
168
|
+
### CategoryGroups ([API Doc](https://pennylane.readme.io/reference/tag-groups-get))
|
169
|
+
```ruby
|
170
|
+
# List all category groups
|
171
|
+
Pennylane::CategoryGroup.list
|
172
|
+
```
|
173
|
+
|
174
|
+
### Per-request api key
|
175
|
+
For apps that need to use multiple keys during the lifetime of a process. it's also possible to set a per-request key:
|
176
|
+
```ruby
|
177
|
+
require "pennylane"
|
178
|
+
|
179
|
+
Pennylane::Customer.list(
|
180
|
+
{},
|
181
|
+
{
|
182
|
+
api_key: 'x1fa....'
|
183
|
+
}
|
184
|
+
)
|
185
|
+
|
186
|
+
Pennylane::Customer.retrieve(
|
187
|
+
'38a1f19a-256d-4692-a8fe-0a16403f59ff',
|
188
|
+
{
|
189
|
+
api_key: 'x1fa....'
|
190
|
+
}
|
191
|
+
)
|
192
|
+
|
193
|
+
```
|
194
|
+
### Accessing resource properties
|
195
|
+
|
196
|
+
Both indexer and accessors can be used to retrieve values of resource properties.
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
customer = Pennylane::Customer.retrieve('customer_id')
|
200
|
+
puts customer['name']
|
201
|
+
puts customer.name
|
202
|
+
|
203
|
+
# NOTE: To do this the gem will try to guess the key of the resource.
|
204
|
+
# Otherwise we will have to do Pennylane::Customer.retrieve('customer_id').customer.name
|
205
|
+
# We rely on `method_missing` to do Pennylane::Customer.retrieve('customer_id').name
|
41
206
|
```
|
42
207
|
|
43
208
|
## Test mode
|
@@ -46,9 +211,30 @@ Pennylane provide a [test environment](https://help.pennylane.com/fr/articles/18
|
|
46
211
|
|
47
212
|
## Development
|
48
213
|
|
49
|
-
|
214
|
+
```bash
|
215
|
+
bundle install
|
216
|
+
bundle exec rake test
|
217
|
+
```
|
218
|
+
|
219
|
+
Resources implemented so far :
|
220
|
+
### CUSTOMER INVOICING
|
221
|
+
|
222
|
+
- Customer Invoices ✅
|
223
|
+
- Estimates 🚧
|
224
|
+
- Billing Subscriptions 🚧
|
225
|
+
|
226
|
+
### REFERENTIALS
|
227
|
+
|
228
|
+
- Customers ✅
|
229
|
+
- Suppliers ✅
|
230
|
+
- Categories ✅
|
231
|
+
- CategoryGroups ✅
|
232
|
+
- Products ✅
|
233
|
+
- Plan Items 🚧
|
234
|
+
- Enums 🚧
|
50
235
|
|
51
|
-
|
236
|
+
### SUPPLIER INVOICING
|
237
|
+
- Supplier Invoices 🚧
|
52
238
|
|
53
239
|
## Contributing
|
54
240
|
|
data/lib/pennylane/client.rb
CHANGED
@@ -3,7 +3,7 @@ module Pennylane
|
|
3
3
|
BASE_URI = 'app.pennylane.com/api/external'.freeze
|
4
4
|
VERSION = 'v1'.freeze
|
5
5
|
|
6
|
-
attr_accessor :
|
6
|
+
attr_accessor :version
|
7
7
|
|
8
8
|
def initialize(key, version: 'v1')
|
9
9
|
@key = key
|
@@ -21,7 +21,7 @@ module Pennylane
|
|
21
21
|
end
|
22
22
|
|
23
23
|
|
24
|
-
def authorization
|
24
|
+
def authorization(key)
|
25
25
|
"Bearer #{key}"
|
26
26
|
end
|
27
27
|
def http
|
@@ -34,10 +34,9 @@ module Pennylane
|
|
34
34
|
Net::HTTP.const_get(method.to_s.capitalize)
|
35
35
|
end
|
36
36
|
|
37
|
-
def request method, path, params
|
38
|
-
req = initialize_request(method, path, params[:query]).tap do |req|
|
39
|
-
req.body = params[:body].to_json if params[:body]
|
40
|
-
# req.query = params if !params.empty? && method == :get
|
37
|
+
def request method, path, params: {}, opts: {}
|
38
|
+
req = initialize_request(method, path, params[:query], opts).tap do |req|
|
39
|
+
req.body = params[:body].to_json if params[:body]
|
41
40
|
end
|
42
41
|
|
43
42
|
http.request(req).tap do |resp|
|
@@ -62,10 +61,11 @@ module Pennylane
|
|
62
61
|
def should_handle_as_error?(code)
|
63
62
|
code.to_i >= 400
|
64
63
|
end
|
65
|
-
|
64
|
+
|
65
|
+
def initialize_request method=nil, path=nil, params={}, opts={}
|
66
66
|
klass(method).new(url(path, params)).tap do |request|
|
67
67
|
request["content-type"] = 'application/json'
|
68
|
-
request["authorization"] = authorization
|
68
|
+
request["authorization"] = authorization(opts.fetch(:api_key, @key))
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
data/lib/pennylane/object.rb
CHANGED
@@ -1,10 +1,7 @@
|
|
1
1
|
module Pennylane
|
2
2
|
class Object
|
3
3
|
include Enumerable
|
4
|
-
|
5
|
-
def initialize(id = nil)
|
6
|
-
@id = id
|
7
|
-
end
|
4
|
+
attr_reader :id
|
8
5
|
|
9
6
|
def initialize_from_response(response, params = {}, opts = {})
|
10
7
|
values = Util.symbolize_names(response)
|
@@ -19,7 +16,7 @@ module Pennylane
|
|
19
16
|
end
|
20
17
|
|
21
18
|
def self.build_from(response, params = {}, opts = {})
|
22
|
-
new
|
19
|
+
new.initialize_from_response(response, params, opts)
|
23
20
|
end
|
24
21
|
|
25
22
|
def self.objects
|
@@ -89,9 +86,7 @@ module Pennylane
|
|
89
86
|
when Pennylane::Object
|
90
87
|
obj.class.build_from(
|
91
88
|
deep_copy(obj.instance_variable_get(:@values)),
|
92
|
-
obj.instance_variable_get(:@opts)
|
93
|
-
Util::OPTS_COPYABLE.include?(k)
|
94
|
-
end
|
89
|
+
obj.instance_variable_get(:@opts)
|
95
90
|
)
|
96
91
|
else
|
97
92
|
obj
|
@@ -1,6 +1,7 @@
|
|
1
1
|
module Pennylane
|
2
2
|
module Resources
|
3
3
|
class Base < Pennylane::Object
|
4
|
+
|
4
5
|
class << self
|
5
6
|
|
6
7
|
def object_name
|
@@ -11,40 +12,74 @@ module Pennylane
|
|
11
12
|
"#{object_name}s"
|
12
13
|
end
|
13
14
|
|
14
|
-
def request_pennylane_object(method:, path:, params: {}, opts: {}, usage: [])
|
15
|
+
def request_pennylane_object(method:, path:, params: {}, opts: {}, usage: [], with: {})
|
15
16
|
resp, opts = execute_resource_request(method, path, params, opts, usage)
|
16
|
-
|
17
|
+
if resp.empty?
|
18
|
+
{}
|
19
|
+
else
|
20
|
+
Util.convert_to_pennylane_object(Util.normalize_response(resp, with), params, opts)
|
21
|
+
end
|
17
22
|
end
|
18
23
|
|
19
24
|
def execute_resource_request(method, path, params = {}, opts = {}, usage = [])
|
20
|
-
api_key = opts.delete(:api_key) || Pennylane.api_key
|
21
|
-
|
22
|
-
|
23
25
|
resp = client.request(
|
24
26
|
method,
|
25
27
|
path,
|
26
28
|
params: params,
|
27
29
|
opts: opts
|
28
30
|
)
|
29
|
-
|
30
|
-
[JSON.parse(resp.read_body), opts]
|
31
|
+
[JSON.parse(resp.read_body || "{}"), opts] # in case body is nil ew return an empty hash
|
31
32
|
end
|
32
33
|
|
33
34
|
def client
|
34
|
-
@client ||=
|
35
|
+
@client ||= {}
|
36
|
+
@client[Pennylane.api_key] ||= Pennylane::Client.new(Pennylane.api_key)
|
35
37
|
end
|
38
|
+
|
39
|
+
def normalize_filters(filters)
|
40
|
+
filters[:filter] = filters[:filter].to_json if filters[:filter]
|
41
|
+
filters
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# object happens to be nil when the object is in a list
|
46
|
+
def id
|
47
|
+
object.source_id
|
48
|
+
rescue
|
49
|
+
super
|
50
|
+
end
|
51
|
+
|
52
|
+
# object happens to be nil when the object is the nested object
|
53
|
+
def [](k)
|
54
|
+
(object && object[k.to_sym]) || @values[k.to_sym]
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
def object
|
59
|
+
@values[self.class.object_name.to_sym]
|
60
|
+
end
|
61
|
+
|
62
|
+
# def inspect
|
63
|
+
# id_string = respond_to?(:id) && !id.nil? ? " id=#{id}" : ""
|
64
|
+
# "#<#{self.class}:0x#{object_id.to_s(16)}#{id_string}> JSON: " +
|
65
|
+
# JSON.pretty_generate(object.instance_variable_get(:@values) || @values)
|
66
|
+
# end
|
67
|
+
|
68
|
+
def update(attributes)
|
69
|
+
resp, opts = self.class.request_pennylane_object(method: :put, path: "/#{self.class.object_name_plural}/#{id}", params: { body: { self.class.object_name => attributes } })
|
70
|
+
@values = resp.instance_variable_get :@values
|
71
|
+
self
|
36
72
|
end
|
37
73
|
|
38
74
|
# So we can call directly method on the object rather than going through his key
|
39
75
|
# Pennylane::Customer.retrieve('any-id').name == Pennylane::Customer.retrieve('any-id').customer.name
|
40
76
|
def method_missing(method_name, *args, &block)
|
41
|
-
|
42
|
-
|
43
|
-
obj.send(method_name, *args, &block)
|
77
|
+
raise NoMethodError, "undefined method `#{method_name}` for #{self.class}" unless object
|
78
|
+
object.send(method_name, *args, &block)
|
44
79
|
end
|
45
80
|
|
46
81
|
def respond_to_missing?(method_name, include_private = false)
|
47
|
-
|
82
|
+
object.respond_to?(method_name) || super
|
48
83
|
end
|
49
84
|
|
50
85
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Pennylane
|
2
|
+
class Category < Resources::Base
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
# override the object_name_plural method otherwise it will return 'categorys'
|
7
|
+
def object_name_plural
|
8
|
+
'categories'
|
9
|
+
end
|
10
|
+
|
11
|
+
def list filters = {}, opts = {}
|
12
|
+
normalize_filters(filters)
|
13
|
+
request_pennylane_object(method: :get, path: "/categories", params: { query: filters }, opts: opts)
|
14
|
+
end
|
15
|
+
|
16
|
+
def retrieve id, opts = {}
|
17
|
+
request_pennylane_object(method: :get, path: "/categories/#{id}", params: {}, opts: opts)
|
18
|
+
end
|
19
|
+
|
20
|
+
def create params, opts = {}
|
21
|
+
request_pennylane_object(method: :post, path: "/categories", params: { body: { object_name => params } }, opts: opts)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Pennylane
|
2
|
+
class CategoryGroup < Resources::Base
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def object_name
|
7
|
+
'category_group'
|
8
|
+
end
|
9
|
+
|
10
|
+
def list filters = {}, opts = {}
|
11
|
+
request_pennylane_object(method: :get, path: "/category_groups", params: { query: filters }, opts: opts)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -4,11 +4,12 @@ module Pennylane
|
|
4
4
|
class << self
|
5
5
|
|
6
6
|
def list filters = {}, opts = {}
|
7
|
+
normalize_filters(filters)
|
7
8
|
request_pennylane_object(method: :get, path: "/customers", params: { query: filters }, opts: opts)
|
8
9
|
end
|
9
10
|
|
10
|
-
def retrieve
|
11
|
-
request_pennylane_object(method: :get, path: "/customers/#{
|
11
|
+
def retrieve id, opts = {}
|
12
|
+
request_pennylane_object(method: :get, path: "/customers/#{id}", params: {}, opts: opts)
|
12
13
|
end
|
13
14
|
|
14
15
|
def create params, opts = {}
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Pennylane
|
2
|
+
class CustomerInvoice < Resources::Base
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def object_name
|
7
|
+
'customer_invoice'
|
8
|
+
end
|
9
|
+
|
10
|
+
def list filters = {}, opts = {}
|
11
|
+
normalize_filters(filters)
|
12
|
+
request_pennylane_object(method: :get, path: "/customer_invoices", params: { query: filters }, opts: opts, with: { invoice: 'customer_invoice' })
|
13
|
+
end
|
14
|
+
|
15
|
+
def retrieve id, opts = {}
|
16
|
+
request_pennylane_object(method: :get, path: "/customer_invoices/#{id}", params: {}, opts: opts, with: { invoice: 'customer_invoice' })
|
17
|
+
end
|
18
|
+
|
19
|
+
def create params, opts = {}
|
20
|
+
request_pennylane_object(method: :post, path: "/customer_invoices", params: { body: params }, opts: opts, with: { invoice: 'customer_invoice' })
|
21
|
+
end
|
22
|
+
|
23
|
+
def import params, opts={}
|
24
|
+
request_pennylane_object(method: :post, path: "/customer_invoices/import", params: { body: params }, opts: opts, with: { invoice: 'customer_invoice' })
|
25
|
+
end
|
26
|
+
|
27
|
+
def links(*quote_group_uuids, opts: {})
|
28
|
+
request_pennylane_object(method: :post, path: "/customer_invoices/links", params: { body: { quote_group_uuids: quote_group_uuids } },
|
29
|
+
opts: {})
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# since object name is different from the class name, we need to override the object_name method
|
34
|
+
def object
|
35
|
+
@values[:invoice]
|
36
|
+
end
|
37
|
+
|
38
|
+
# doesnt have a `source_id` so we override it
|
39
|
+
def id
|
40
|
+
object.id
|
41
|
+
end
|
42
|
+
|
43
|
+
# API CALLS
|
44
|
+
|
45
|
+
# since object name is different from the class name, we need to override the method
|
46
|
+
def update(attributes)
|
47
|
+
resp, opts = self.class.request_pennylane_object(method: :put,
|
48
|
+
path: "/customer_invoices/#{id}",
|
49
|
+
params: { body: { 'invoice' => attributes } },
|
50
|
+
opts: {}, with: { invoice: 'customer_invoice' })
|
51
|
+
@values = resp.instance_variable_get :@values
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def finalize
|
56
|
+
request_and_retrieve(method: :put, path: "/customer_invoices/#{id}", action: 'finalize')
|
57
|
+
end
|
58
|
+
|
59
|
+
def mark_as_paid
|
60
|
+
resp, opts = self.class.request_pennylane_object(method: :put,
|
61
|
+
path: "/customer_invoices/#{id}/mark_as_paid",
|
62
|
+
params: {},
|
63
|
+
opts: {}, with: { invoice: 'customer_invoice' })
|
64
|
+
@values = resp.instance_variable_get :@values
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def send_by_email
|
69
|
+
request_and_retrieve(method: :post, path: "/customer_invoices/#{id}", action: 'send_by_email')
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# When API returns an empty body
|
75
|
+
# so we need to skip values assignment from the response
|
76
|
+
# GET /customer_invoices/:id again to get the updated values
|
77
|
+
def request_and_retrieve(method:, path:, action:)
|
78
|
+
self.class.request_pennylane_object(method: method,
|
79
|
+
path: "#{path}/#{action}",
|
80
|
+
params: {},
|
81
|
+
opts: {}, with: { invoice: 'customer_invoice' })
|
82
|
+
resp, opts = self.class.request_pennylane_object(method: :get,
|
83
|
+
path: path,
|
84
|
+
params: {},
|
85
|
+
opts: {}, with: { invoice: 'customer_invoice' })
|
86
|
+
@values = resp.instance_variable_get :@values
|
87
|
+
self
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Pennylane
|
2
|
+
class Product < Resources::Base
|
3
|
+
|
4
|
+
class << self
|
5
|
+
|
6
|
+
def list filters = {}, opts = {}
|
7
|
+
normalize_filters(filters)
|
8
|
+
request_pennylane_object(method: :get, path: "/products", params: { query: filters }, opts: opts)
|
9
|
+
end
|
10
|
+
|
11
|
+
def retrieve id, opts = {}
|
12
|
+
request_pennylane_object(method: :get, path: "/products/#{id}", params: {}, opts: opts)
|
13
|
+
end
|
14
|
+
|
15
|
+
def create params, opts = {}
|
16
|
+
request_pennylane_object(method: :post, path: "/products", params: { body: { product: params } }, opts: opts)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Pennylane
|
2
|
+
class Supplier < Resources::Base
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def list filters = {}, opts = {}
|
6
|
+
normalize_filters(filters)
|
7
|
+
request_pennylane_object(method: :get, path: "/suppliers", params: { query: filters }, opts: opts)
|
8
|
+
end
|
9
|
+
|
10
|
+
def retrieve supplier_id, opts = {}
|
11
|
+
request_pennylane_object(method: :get, path: "/suppliers/#{supplier_id}", params: {}, opts: opts)
|
12
|
+
end
|
13
|
+
|
14
|
+
def create params, opts = {}
|
15
|
+
request_pennylane_object(method: :post, path: "/suppliers", params: { body: { object_name => params } }, opts: opts)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/pennylane/util.rb
CHANGED
@@ -20,34 +20,49 @@ module Pennylane
|
|
20
20
|
# It will add an _object key to each hash in the response
|
21
21
|
# This key will contain the name of the object
|
22
22
|
# It will also add an _object key to the root of the response
|
23
|
-
|
23
|
+
# with: is used to map the object name to a different class
|
24
|
+
# Example : GET /customer_invoices will return a list of invoices
|
25
|
+
# {
|
26
|
+
# "total_pages": 5,
|
27
|
+
# "current_page": 1,
|
28
|
+
# "total_invoices": 12,
|
29
|
+
# "invoices": []
|
30
|
+
# }
|
31
|
+
# `invoices` should be `customer_invoice` so we can cast it to the right class CustomerInvoice
|
32
|
+
# Since we don't have the ability to change the API response.
|
33
|
+
# We can achieve this by calling normalize_response(response, with: {invoice: 'customer_invoice'})
|
34
|
+
def normalize_response(object, with={})
|
24
35
|
# puts object.inspect
|
25
36
|
case object
|
26
37
|
when Hash
|
27
38
|
new_hash = {}
|
28
|
-
new_hash['_object'] = object.has_key?('total_pages') ? 'list' : singularize(object.keys.first)
|
29
|
-
|
39
|
+
new_hash['_object'] = object.has_key?('total_pages') ? 'list' : (with[singularize(object.keys.first).to_sym] || singularize(object.keys.first))
|
30
40
|
object.each do |key, value|
|
31
41
|
if value.is_a? Array
|
32
42
|
new_hash[key] = value.map do |h|
|
33
|
-
h['_object'] = singularize(key) if h.is_a? Hash
|
34
|
-
normalize_response(h)
|
43
|
+
h['_object'] = with[singularize(key).to_sym] || singularize(key) if h.is_a? Hash
|
44
|
+
normalize_response(h, with)
|
35
45
|
end
|
36
46
|
elsif value.is_a? Hash
|
37
|
-
value['_object'] = singularize(key)
|
47
|
+
value['_object'] = with[singularize(key).to_sym] || singularize(key)
|
38
48
|
end
|
39
|
-
new_hash[key] = normalize_response(value)
|
49
|
+
new_hash[key] = normalize_response(value, with)
|
40
50
|
end
|
41
51
|
new_hash
|
42
52
|
when Array
|
43
53
|
object.map do |value|
|
44
|
-
normalize_response(value)
|
54
|
+
normalize_response(value, with)
|
45
55
|
end
|
46
56
|
else
|
47
57
|
object
|
48
58
|
end
|
49
59
|
end
|
50
60
|
|
61
|
+
def file(file_path)
|
62
|
+
file_data = File.open(file_path).read
|
63
|
+
Base64.strict_encode64(file_data)
|
64
|
+
end
|
65
|
+
|
51
66
|
# This method is used to convert the keys of a hash from strings to symbols
|
52
67
|
def symbolize_names(object)
|
53
68
|
case object
|
data/lib/pennylane/version.rb
CHANGED
data/lib/pennylane.rb
CHANGED
@@ -22,7 +22,12 @@ module Pennylane
|
|
22
22
|
|
23
23
|
API_RESOURCES = {
|
24
24
|
ListObject.object_name => ListObject,
|
25
|
-
|
25
|
+
Category.object_name => Category,
|
26
|
+
CategoryGroup.object_name => CategoryGroup,
|
27
|
+
Customer.object_name => Customer,
|
28
|
+
CustomerInvoice.object_name => CustomerInvoice,
|
29
|
+
Product.object_name => Product,
|
30
|
+
Supplier.object_name => Supplier
|
26
31
|
}.freeze
|
27
32
|
|
28
33
|
@config = Pennylane::Configuration.new
|
data/pennylane.gemspec
CHANGED
@@ -34,5 +34,6 @@ Gem::Specification.new do |spec|
|
|
34
34
|
spec.add_development_dependency "vcr", "~> 6.2"
|
35
35
|
spec.add_development_dependency "test-unit", "~> 3.6"
|
36
36
|
spec.add_development_dependency "minitest", "~> 5.22"
|
37
|
+
spec.add_development_dependency "activesupport", "~> 7.1"
|
37
38
|
# guide at: https://bundler.io/guides/creating_gem.html
|
38
39
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pennylane
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephane Bounmy
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-04-
|
11
|
+
date: 2024-04-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: vcr
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '5.22'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activesupport
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '7.1'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '7.1'
|
55
69
|
description: Pennylane offers integrated financial management and accounting tools
|
56
70
|
for businesses. See https://pennylane.com for details.
|
57
71
|
email:
|
@@ -74,7 +88,12 @@ files:
|
|
74
88
|
- lib/pennylane/list_object.rb
|
75
89
|
- lib/pennylane/object.rb
|
76
90
|
- lib/pennylane/resources/base.rb
|
91
|
+
- lib/pennylane/resources/category.rb
|
92
|
+
- lib/pennylane/resources/category_group.rb
|
77
93
|
- lib/pennylane/resources/customer.rb
|
94
|
+
- lib/pennylane/resources/customer_invoice.rb
|
95
|
+
- lib/pennylane/resources/product.rb
|
96
|
+
- lib/pennylane/resources/supplier.rb
|
78
97
|
- lib/pennylane/util.rb
|
79
98
|
- lib/pennylane/version.rb
|
80
99
|
- pennylane.gemspec
|