pennylane 0.2.0.pre.alpha → 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 +12 -0
- data/Gemfile.lock +1 -1
- data/README.md +145 -5
- data/lib/pennylane/client.rb +7 -6
- data/lib/pennylane/object.rb +1 -3
- data/lib/pennylane/resources/base.rb +14 -7
- data/lib/pennylane/resources/customer_invoice.rb +52 -7
- data/lib/pennylane/util.rb +5 -0
- data/lib/pennylane/version.rb +1 -1
- metadata +4 -4
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,17 @@
|
|
|
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
|
+
|
|
3
15
|
## [0.2.0-alpha] - 2024-04-20
|
|
4
16
|
|
|
5
17
|
- Support resources
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -32,7 +32,11 @@ 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
|
|
|
@@ -43,9 +47,131 @@ Pennylane::Customer.retrieve('38a1f19a-256d-4692-a8fe-0a16403f59ff')
|
|
|
43
47
|
cus = Pennylane::Customer.retrieve('38a1f19a-256d-4692-a8fe-0a16403f59ff')
|
|
44
48
|
cus.update(name: 'Apple Inc')
|
|
45
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
|
|
46
134
|
```
|
|
47
135
|
|
|
48
|
-
###
|
|
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
|
|
49
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:
|
|
50
176
|
```ruby
|
|
51
177
|
require "pennylane"
|
|
@@ -64,6 +190,19 @@ Pennylane::Customer.retrieve(
|
|
|
64
190
|
}
|
|
65
191
|
)
|
|
66
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
|
|
67
206
|
```
|
|
68
207
|
|
|
69
208
|
## Test mode
|
|
@@ -72,14 +211,15 @@ Pennylane provide a [test environment](https://help.pennylane.com/fr/articles/18
|
|
|
72
211
|
|
|
73
212
|
## Development
|
|
74
213
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
214
|
+
```bash
|
|
215
|
+
bundle install
|
|
216
|
+
bundle exec rake test
|
|
217
|
+
```
|
|
78
218
|
|
|
79
219
|
Resources implemented so far :
|
|
80
220
|
### CUSTOMER INVOICING
|
|
81
221
|
|
|
82
|
-
- Customer Invoices
|
|
222
|
+
- Customer Invoices ✅
|
|
83
223
|
- Estimates 🚧
|
|
84
224
|
- Billing Subscriptions 🚧
|
|
85
225
|
|
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,8 +34,8 @@ 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|
|
|
37
|
+
def request method, path, params: {}, opts: {}
|
|
38
|
+
req = initialize_request(method, path, params[:query], opts).tap do |req|
|
|
39
39
|
req.body = params[:body].to_json if params[:body]
|
|
40
40
|
end
|
|
41
41
|
|
|
@@ -61,10 +61,11 @@ module Pennylane
|
|
|
61
61
|
def should_handle_as_error?(code)
|
|
62
62
|
code.to_i >= 400
|
|
63
63
|
end
|
|
64
|
-
|
|
64
|
+
|
|
65
|
+
def initialize_request method=nil, path=nil, params={}, opts={}
|
|
65
66
|
klass(method).new(url(path, params)).tap do |request|
|
|
66
67
|
request["content-type"] = 'application/json'
|
|
67
|
-
request["authorization"] = authorization
|
|
68
|
+
request["authorization"] = authorization(opts.fetch(:api_key, @key))
|
|
68
69
|
end
|
|
69
70
|
end
|
|
70
71
|
|
data/lib/pennylane/object.rb
CHANGED
|
@@ -86,9 +86,7 @@ module Pennylane
|
|
|
86
86
|
when Pennylane::Object
|
|
87
87
|
obj.class.build_from(
|
|
88
88
|
deep_copy(obj.instance_variable_get(:@values)),
|
|
89
|
-
obj.instance_variable_get(:@opts)
|
|
90
|
-
Util::OPTS_COPYABLE.include?(k)
|
|
91
|
-
end
|
|
89
|
+
obj.instance_variable_get(:@opts)
|
|
92
90
|
)
|
|
93
91
|
else
|
|
94
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
|
|
@@ -13,25 +14,26 @@ module Pennylane
|
|
|
13
14
|
|
|
14
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
|
|
36
38
|
|
|
37
39
|
def normalize_filters(filters)
|
|
@@ -47,6 +49,11 @@ module Pennylane
|
|
|
47
49
|
super
|
|
48
50
|
end
|
|
49
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
|
+
|
|
50
57
|
#
|
|
51
58
|
def object
|
|
52
59
|
@values[self.class.object_name.to_sym]
|
|
@@ -20,26 +20,71 @@ module Pennylane
|
|
|
20
20
|
request_pennylane_object(method: :post, path: "/customer_invoices", params: { body: params }, opts: opts, with: { invoice: 'customer_invoice' })
|
|
21
21
|
end
|
|
22
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]
|
|
23
36
|
end
|
|
24
37
|
|
|
38
|
+
# doesnt have a `source_id` so we override it
|
|
39
|
+
def id
|
|
40
|
+
object.id
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# API CALLS
|
|
44
|
+
|
|
25
45
|
# since object name is different from the class name, we need to override the method
|
|
26
46
|
def update(attributes)
|
|
27
|
-
resp, opts = self.class.request_pennylane_object(method: :put,
|
|
47
|
+
resp, opts = self.class.request_pennylane_object(method: :put,
|
|
48
|
+
path: "/customer_invoices/#{id}",
|
|
28
49
|
params: { body: { 'invoice' => attributes } },
|
|
29
50
|
opts: {}, with: { invoice: 'customer_invoice' })
|
|
30
51
|
@values = resp.instance_variable_get :@values
|
|
31
52
|
self
|
|
32
53
|
end
|
|
33
54
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@values[:invoice]
|
|
55
|
+
def finalize
|
|
56
|
+
request_and_retrieve(method: :put, path: "/customer_invoices/#{id}", action: 'finalize')
|
|
37
57
|
end
|
|
38
58
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
42
66
|
end
|
|
43
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
|
|
44
89
|
end
|
|
45
90
|
end
|
data/lib/pennylane/util.rb
CHANGED
|
@@ -58,6 +58,11 @@ module Pennylane
|
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
def file(file_path)
|
|
62
|
+
file_data = File.open(file_path).read
|
|
63
|
+
Base64.strict_encode64(file_data)
|
|
64
|
+
end
|
|
65
|
+
|
|
61
66
|
# This method is used to convert the keys of a hash from strings to symbols
|
|
62
67
|
def symbolize_names(object)
|
|
63
68
|
case object
|
data/lib/pennylane/version.rb
CHANGED
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: 0.
|
|
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
|
|
@@ -116,9 +116,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
116
116
|
version: 2.6.0
|
|
117
117
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
118
118
|
requirements:
|
|
119
|
-
- - "
|
|
119
|
+
- - ">="
|
|
120
120
|
- !ruby/object:Gem::Version
|
|
121
|
-
version:
|
|
121
|
+
version: '0'
|
|
122
122
|
requirements: []
|
|
123
123
|
rubygems_version: 3.4.10
|
|
124
124
|
signing_key:
|