moco-ruby 1.3.0 → 1.4.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 +19 -0
- data/Gemfile.lock +1 -1
- data/README.md +99 -3
- data/Rakefile +6 -0
- data/lib/moco/client.rb +6 -2
- data/lib/moco/collection_proxy.rb +12 -6
- data/lib/moco/connection.rb +8 -2
- data/lib/moco/entities/absence_request.rb +10 -0
- data/lib/moco/entities/approval.rb +14 -0
- data/lib/moco/entities/base_entity.rb +10 -5
- data/lib/moco/entities/catalog_service.rb +13 -0
- data/lib/moco/entities/catalog_service_item.rb +7 -0
- data/lib/moco/entities/company.rb +10 -0
- data/lib/moco/entities/invoice.rb +2 -2
- data/lib/moco/entities/offer.rb +17 -0
- data/lib/moco/entities/offer_approval.rb +1 -1
- data/lib/moco/entities/offer_confirmation.rb +18 -0
- data/lib/moco/entities/planned_purchase.rb +18 -0
- data/lib/moco/entities/purchase.rb +3 -3
- data/lib/moco/entities/purchase_budget.rb +6 -3
- data/lib/moco/entities/purchase_draft.rb +11 -4
- data/lib/moco/entities/recurring_expense.rb +8 -0
- data/lib/moco/entities/revenue_category.rb +10 -0
- data/lib/moco/entities/todo.rb +7 -0
- data/lib/moco/nested_collection_proxy.rb +3 -2
- data/lib/moco/version.rb +1 -1
- data/lib/moco.rb +7 -0
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 05ee5448c599c6d3aa08e1b3ee7f39937fdd83504e1448058101442f2ea0b9f8
|
|
4
|
+
data.tar.gz: 66fef5ab73ebc81eaa8a8559ec4fd492fccf75e301ff8e0c7de7e3f0a1442caa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 693ac37ba7c053009c6c86e390e34acc76a63c4f0bf63e26c42da9045b26447a8c032753dc86c21f324ac49fbe9f8fa387d65aa2d0093dd5335b268510d976e0
|
|
7
|
+
data.tar.gz: 8f6fe03c4d08fe908ea307c5ca00133b033ebb532100b2e40974a39ec64b2b29ebea3ce2f6ad2cc1836444fa82b4607944203cc63e5e53f03f8a673963496910
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.4.0] - 2026-10-06
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Offer confirmations with CRUD and PDF downloads; revenue categories with CRUD.
|
|
9
|
+
- Planned purchases, invoice/purchase approvals, absence requests, and read-only to-dos.
|
|
10
|
+
- Company archive/unarchive, recurring expense execution, purchase draft PDF downloads,
|
|
11
|
+
purchase budget annual targets, catalog service item operations, and offer customer approval helpers.
|
|
12
|
+
- Planned-versus-tracked report and optional Bearer authentication (`auth_scheme: :bearer`).
|
|
13
|
+
- Mocked regression tests for API resources, actions, fields, and filters; `rake test` runs the mocked suite.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Entity updates accept fields absent from the response; saved and reloaded entities expose response attributes.
|
|
17
|
+
- Nested entities use their parent collection for save, reload, and deletion.
|
|
18
|
+
- Account-level nested resources use their parent's API path.
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
- Document offer editing/deletion, draft creation/deletion, writable budgets and revenue
|
|
22
|
+
categories, and additional API fields and filters from the current OpenAPI reference.
|
|
23
|
+
|
|
5
24
|
## [1.3.0] - 2026-05-23
|
|
6
25
|
|
|
7
26
|
### Added
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -217,6 +217,95 @@ moco.offers.find(123).attachments.create(
|
|
|
217
217
|
)
|
|
218
218
|
```
|
|
219
219
|
|
|
220
|
+
### API Resources and Actions (2025–2026)
|
|
221
|
+
|
|
222
|
+
The [OpenAPI reference](https://docs.mocoapp.com/api/docs/v1) defines supported
|
|
223
|
+
operations, required fields, permissions, and filters. Collections accept API fields
|
|
224
|
+
unchanged through `create`, `update`, and `where`; response fields are available as
|
|
225
|
+
entity attributes. Only call operations documented for that resource (for example,
|
|
226
|
+
`todos` is read-only, and invoices do not have a general update endpoint).
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
# Offer confirmations: list, find, create, update, delete, and PDF download
|
|
230
|
+
confirmation = moco.offer_confirmations.create(offer_id: 123, title: "Order confirmation")
|
|
231
|
+
confirmation.update(footer_raw: "<p>Thank you!</p>")
|
|
232
|
+
confirmation.pdf(letter_paper_id: 42)
|
|
233
|
+
moco.offers.find(123).confirmations.all
|
|
234
|
+
|
|
235
|
+
# Offers support full updates and deletion
|
|
236
|
+
moco.offers.update(123, title: "Website proposal", items: [...])
|
|
237
|
+
moco.offers.delete(123)
|
|
238
|
+
|
|
239
|
+
# Revenue categories: list, find, create, update, and delete
|
|
240
|
+
moco.revenue_categories.where(term: "Consulting").all
|
|
241
|
+
moco.revenue_categories.create(name: "Consulting", assignment_kind: "tasks_and_expenses")
|
|
242
|
+
|
|
243
|
+
# Purchase drafts: list, find, create, delete, and PDF download
|
|
244
|
+
moco.purchase_drafts.create(
|
|
245
|
+
title: "Supplier invoice",
|
|
246
|
+
file: { filename: "invoice.pdf", base64: Base64.strict_encode64(File.binread("invoice.pdf")) }
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Company archiving and early execution of recurring additional services
|
|
250
|
+
moco.companies.find(123).archive
|
|
251
|
+
moco.companies.find(123).unarchive
|
|
252
|
+
moco.projects.find(123).recurring_expenses.find(456).recur
|
|
253
|
+
|
|
254
|
+
# Purchase budgets support creation, updates, deletion, and annual targets
|
|
255
|
+
moco.purchase_budgets.where(year: 2026).all
|
|
256
|
+
budget = moco.purchase_budgets.create(title: "Infrastructure")
|
|
257
|
+
budget.update_target(target_amount: 12_000, year: 2026)
|
|
258
|
+
|
|
259
|
+
# Planned purchases support list, find, create, update, and delete
|
|
260
|
+
moco.planned_purchases.create(
|
|
261
|
+
date: "2026-08-01", title: "Hosting", net_total: 100, currency: "EUR", user_id: 123
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Invoice/purchase approvals: list, find, create, delete, approve, and deny
|
|
265
|
+
approval = moco.approvals.create(target_type: "purchase", target_id: 123, approver_id: 456)
|
|
266
|
+
approval.approve
|
|
267
|
+
# Or: approval.deny(note: "Please check the amount")
|
|
268
|
+
|
|
269
|
+
# Absence requests: list, find, create, and delete
|
|
270
|
+
moco.absence_requests.where(user_id: 123).all
|
|
271
|
+
moco.absence_requests.find(456).destroy(comment: "Cancelled")
|
|
272
|
+
|
|
273
|
+
# To-dos are read-only
|
|
274
|
+
moco.todos.where(completed: false).all
|
|
275
|
+
moco.todos.find(123)
|
|
276
|
+
|
|
277
|
+
# Catalog items are listed in the parent; individual items support CRUD
|
|
278
|
+
service = moco.catalog_services.find(123)
|
|
279
|
+
service.items.all
|
|
280
|
+
service.items.create(type: "item", title: "Consulting", quantity: 1, unit: "h", unit_price: 150)
|
|
281
|
+
service.items.find(456).update(unit_cost: 75)
|
|
282
|
+
|
|
283
|
+
# Online customer approval for offers
|
|
284
|
+
moco.offers.find(123).activate_customer_approval
|
|
285
|
+
moco.offers.find(123).customer_approval
|
|
286
|
+
moco.offers.find(123).deactivate_customer_approval
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
New fields need no schema configuration. Examples include `skip_favorite` on projects,
|
|
290
|
+
`task_id` on planning entries, `invoice_format`, `default_payment_means`,
|
|
291
|
+
`customer_vat_code_id`, `supplier_vat_code_id`, `bank_owner`, and `bank_bic` on companies,
|
|
292
|
+
`welcome_email` on users, and `created_at` on comments. User creation does not require
|
|
293
|
+
`password`. Filter users by `email` and schedules by `absence_request_id`.
|
|
294
|
+
|
|
295
|
+
Use `vat_code_id` on invoices and purchase items rather than the deprecated `tax`
|
|
296
|
+
value. Offer/invoice items accept `revenue_category_id`; offer items also accept
|
|
297
|
+
`unit_cost`. Project expenses accept `revenue_category_id`, `service_period_from`,
|
|
298
|
+
`service_period_to`, `planned_purchase_date`, and `cost_total_planned`.
|
|
299
|
+
Response fields such as activity `worked_seconds`, reminder `status` / `file_url`,
|
|
300
|
+
invoice `creditor_reference`, comment `impersonating_user`, and draft `net_total` /
|
|
301
|
+
`gross_total` are exposed automatically.
|
|
302
|
+
|
|
303
|
+
Bearer authentication is optional; Token authentication is the default:
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
moco = MOCO::Client.new(subdomain: "your-subdomain", api_key: "your-api-key", auth_scheme: :bearer)
|
|
307
|
+
```
|
|
308
|
+
|
|
220
309
|
### Reports
|
|
221
310
|
|
|
222
311
|
Access read-only report endpoints:
|
|
@@ -231,6 +320,11 @@ utilization = moco.reports.utilization(from: "2024-01-01", to: "2024-12-31")
|
|
|
231
320
|
# Financial reports
|
|
232
321
|
cashflow = moco.reports.cashflow(from: "2024-01-01", to: "2024-03-31")
|
|
233
322
|
finance = moco.reports.finance(from: "2024-01-01", to: "2024-03-31")
|
|
323
|
+
|
|
324
|
+
# Planned versus tracked hours (optional date range and filters)
|
|
325
|
+
planned_vs_tracked = moco.reports.planned_vs_tracked(
|
|
326
|
+
from: "2026-01-01", to: "2026-01-31", user_ids: "123,456", project_id: 789
|
|
327
|
+
)
|
|
234
328
|
```
|
|
235
329
|
|
|
236
330
|
### Supported Entities
|
|
@@ -241,19 +335,19 @@ The gem supports all MOCO API entities with a Ruby-esque interface:
|
|
|
241
335
|
`Project`, `Activity`, `User`, `Company`, `Task`, `Invoice`, `Deal`, `Expense`, `WebHook`, `Schedule`, `Presence`, `Holiday`, `PlanningEntry`
|
|
242
336
|
|
|
243
337
|
**Business:**
|
|
244
|
-
`Contact`, `Offer`, `Purchase`, `Receipt`, `Comment`, `Tag`, `Tagging`, `DealCategory`, `ProjectGroup`, `Unit`
|
|
338
|
+
`Contact`, `Offer`, `OfferConfirmation`, `Purchase`, `PlannedPurchase`, `Receipt`, `Comment`, `Tag`, `Tagging`, `DealCategory`, `ProjectGroup`, `Unit`, `Approval`, `AbsenceRequest`, `Todo` (read-only)
|
|
245
339
|
|
|
246
340
|
**Account Settings:**
|
|
247
341
|
`CatalogService`, `CustomProperty`, `ExpenseTemplate`, `FixedCost`, `HourlyRate`, `InternalHourlyRate`, `TaskTemplate`, `UserRole`
|
|
248
342
|
|
|
249
343
|
**Financial:**
|
|
250
|
-
`VatCodeSale`, `VatCodePurchase`, `PurchaseCategory`, `PurchaseDraft`, `PurchaseBudget`, `PurchasePayment`
|
|
344
|
+
`VatCodeSale`, `VatCodePurchase`, `RevenueCategory`, `PurchaseCategory`, `PurchaseDraft`, `PurchaseBudget`, `PurchasePayment`
|
|
251
345
|
|
|
252
346
|
**Bookkeeping:**
|
|
253
347
|
`InvoiceBookkeepingExport`, `PurchaseBookkeepingExport`
|
|
254
348
|
|
|
255
349
|
**Nested Resources:**
|
|
256
|
-
`Employment`, `WorkTimeAdjustment`, `ProjectContract`, `PaymentSchedule`, `RecurringExpense`, `InvoicePayment`, `InvoiceReminder`, `OfferApproval`, `InvoiceAttachment`, `OfferAttachment`
|
|
350
|
+
`Employment`, `WorkTimeAdjustment`, `ProjectContract`, `PaymentSchedule`, `RecurringExpense`, `InvoicePayment`, `InvoiceReminder`, `OfferApproval`, `InvoiceAttachment`, `OfferAttachment`, `CatalogServiceItem`
|
|
257
351
|
|
|
258
352
|
**Misc:**
|
|
259
353
|
`LetterPaper` (read-only), `Session` (for API key exchange/verification)
|
|
@@ -323,6 +417,8 @@ The gem includes unit tests (mocked) and integration tests (live API):
|
|
|
323
417
|
|
|
324
418
|
```bash
|
|
325
419
|
# Unit tests (mocked, fast)
|
|
420
|
+
bundle exec rake test
|
|
421
|
+
bundle exec ruby -Ilib -Itest test/test_api_updates.rb
|
|
326
422
|
bundle exec ruby -Ilib -Itest test/test_v2_api.rb
|
|
327
423
|
bundle exec ruby -Ilib -Itest test/test_new_entities.rb
|
|
328
424
|
|
data/Rakefile
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
4
|
require "rubocop/rake_task"
|
|
5
|
+
require "rake/testtask"
|
|
6
|
+
|
|
7
|
+
Rake::TestTask.new(:test) do |task|
|
|
8
|
+
task.libs << "lib" << "test"
|
|
9
|
+
task.test_files = FileList["test/test_v2_api.rb", "test/test_new_entities.rb", "test/test_api_updates.rb"]
|
|
10
|
+
end
|
|
5
11
|
|
|
6
12
|
RuboCop::RakeTask.new
|
|
7
13
|
|
data/lib/moco/client.rb
CHANGED
|
@@ -6,8 +6,8 @@ module MOCO
|
|
|
6
6
|
class Client
|
|
7
7
|
attr_reader :connection
|
|
8
8
|
|
|
9
|
-
def initialize(subdomain:, api_key:, debug: false)
|
|
10
|
-
@connection = Connection.new(self, subdomain, api_key, debug: debug)
|
|
9
|
+
def initialize(subdomain:, api_key:, debug: false, auth_scheme: :token)
|
|
10
|
+
@connection = Connection.new(self, subdomain, api_key, debug: debug, auth_scheme: auth_scheme)
|
|
11
11
|
@collections = {}
|
|
12
12
|
end
|
|
13
13
|
|
|
@@ -102,6 +102,10 @@ module MOCO
|
|
|
102
102
|
@client.get("report/finance", params)
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
def planned_vs_tracked(from: nil, to: nil, **filters)
|
|
106
|
+
@client.get("report/planned_vs_tracked", { from:, to: }.merge(filters).compact)
|
|
107
|
+
end
|
|
108
|
+
|
|
105
109
|
# Get utilization report
|
|
106
110
|
# @param from [String] start date (YYYY-MM-DD) - required
|
|
107
111
|
# @param to [String] end date (YYYY-MM-DD) - required
|
|
@@ -112,18 +112,18 @@ module MOCO
|
|
|
112
112
|
klass = entity_class
|
|
113
113
|
return nil unless klass && klass <= MOCO::BaseEntity
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
build_entity(client.post(@base_path, attributes))
|
|
116
116
|
end
|
|
117
117
|
|
|
118
118
|
def update(id, attributes)
|
|
119
119
|
klass = entity_class
|
|
120
120
|
return nil unless klass && klass <= MOCO::BaseEntity
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
build_entity(client.put("#{@base_path}/#{id}", attributes))
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
-
def delete(id)
|
|
126
|
-
client.delete("#{@base_path}/#{id}")
|
|
125
|
+
def delete(id, params = {})
|
|
126
|
+
client.delete("#{@base_path}/#{id}", params)
|
|
127
127
|
end
|
|
128
128
|
|
|
129
129
|
# --- Internal Methods ---
|
|
@@ -178,6 +178,12 @@ module MOCO
|
|
|
178
178
|
|
|
179
179
|
private
|
|
180
180
|
|
|
181
|
+
def build_entity(data)
|
|
182
|
+
entity = entity_class.new(client, data)
|
|
183
|
+
entity.collection = self
|
|
184
|
+
entity
|
|
185
|
+
end
|
|
186
|
+
|
|
181
187
|
# Wraps the raw API response (Hash or Array of Hashes) into entity objects.
|
|
182
188
|
def wrap_response(response_body)
|
|
183
189
|
klass = entity_class
|
|
@@ -186,10 +192,10 @@ module MOCO
|
|
|
186
192
|
|
|
187
193
|
if response_body.is_a?(Array)
|
|
188
194
|
# Convert array of hashes to array of entity objects
|
|
189
|
-
response_body.map { |item_hash|
|
|
195
|
+
response_body.map { |item_hash| build_entity(item_hash) if item_hash.is_a?(Hash) }.compact
|
|
190
196
|
elsif response_body.is_a?(Hash)
|
|
191
197
|
# Wrap single hash response in an array for consistency internally
|
|
192
|
-
[
|
|
198
|
+
[build_entity(response_body)]
|
|
193
199
|
else
|
|
194
200
|
# Handle unexpected response types (like the String error we saw)
|
|
195
201
|
warn "Warning: Unexpected API response type received in wrap_response: #{response_body.class}. Expected Hash or Array."
|
data/lib/moco/connection.rb
CHANGED
|
@@ -9,7 +9,9 @@ module MOCO
|
|
|
9
9
|
class Connection
|
|
10
10
|
attr_reader :client, :subdomain, :api_key, :debug
|
|
11
11
|
|
|
12
|
-
def initialize(client, subdomain, api_key, debug: false)
|
|
12
|
+
def initialize(client, subdomain, api_key, debug: false, auth_scheme: :token)
|
|
13
|
+
raise ArgumentError, "auth_scheme must be :token or :bearer" unless %i[token bearer].include?(auth_scheme)
|
|
14
|
+
|
|
13
15
|
@client = client
|
|
14
16
|
@subdomain = subdomain
|
|
15
17
|
@api_key = api_key
|
|
@@ -17,7 +19,11 @@ module MOCO
|
|
|
17
19
|
@conn = Faraday.new do |f|
|
|
18
20
|
f.request :json
|
|
19
21
|
f.response :json
|
|
20
|
-
|
|
22
|
+
if auth_scheme == :bearer
|
|
23
|
+
f.request :authorization, "Bearer", @api_key
|
|
24
|
+
else
|
|
25
|
+
f.request :authorization, "Token", "token=#{@api_key}"
|
|
26
|
+
end
|
|
21
27
|
f.url_prefix = "https://#{@subdomain}.mocoapp.com/api/v1"
|
|
22
28
|
end
|
|
23
29
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOCO
|
|
4
|
+
# Approval request for an invoice or purchase.
|
|
5
|
+
class Approval < BaseEntity
|
|
6
|
+
def approve
|
|
7
|
+
self.class.new(client, client.post("approvals/#{id}/approve"))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def deny(note: nil)
|
|
11
|
+
self.class.new(client, client.post("approvals/#{id}/deny", { note: }.compact))
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -6,6 +6,7 @@ module MOCO
|
|
|
6
6
|
# Base class for all MOCO API entities
|
|
7
7
|
class BaseEntity
|
|
8
8
|
attr_reader :client, :attributes
|
|
9
|
+
attr_writer :collection
|
|
9
10
|
|
|
10
11
|
# Initializes an entity instance from raw API response data (Hash).
|
|
11
12
|
# Recursively processes nested hashes and arrays, converting known
|
|
@@ -76,10 +77,11 @@ module MOCO
|
|
|
76
77
|
# Check if the client responds to the collection method
|
|
77
78
|
if client.respond_to?(collection_name)
|
|
78
79
|
# Use the collection proxy to update the entity
|
|
79
|
-
updated_data = client.send(collection_name).update(id,
|
|
80
|
+
updated_data = (@collection || client.send(collection_name)).update(id, to_h)
|
|
80
81
|
|
|
81
82
|
# Update local attributes with the response data
|
|
82
83
|
@attributes = updated_data.attributes if updated_data
|
|
84
|
+
define_attribute_methods
|
|
83
85
|
else
|
|
84
86
|
warn "Warning: Client does not respond to collection '#{collection_name}' for saving entity."
|
|
85
87
|
end
|
|
@@ -92,8 +94,9 @@ module MOCO
|
|
|
92
94
|
def update(new_attributes)
|
|
93
95
|
# Update attributes
|
|
94
96
|
new_attributes.each do |key, value|
|
|
95
|
-
|
|
97
|
+
attributes[key.to_sym] = process_value(value, key.to_sym)
|
|
96
98
|
end
|
|
99
|
+
define_attribute_methods
|
|
97
100
|
|
|
98
101
|
# Save changes
|
|
99
102
|
save
|
|
@@ -101,7 +104,7 @@ module MOCO
|
|
|
101
104
|
|
|
102
105
|
# Deletes the entity from the API.
|
|
103
106
|
# Returns true on success, false on failure.
|
|
104
|
-
def destroy
|
|
107
|
+
def destroy(**params)
|
|
105
108
|
return false if id.nil? # Can't destroy without an ID
|
|
106
109
|
|
|
107
110
|
# Determine the collection name from the class name
|
|
@@ -110,7 +113,8 @@ module MOCO
|
|
|
110
113
|
# Check if the client responds to the collection method
|
|
111
114
|
if client.respond_to?(collection_name)
|
|
112
115
|
# Use the collection proxy to delete the entity
|
|
113
|
-
client.send(collection_name)
|
|
116
|
+
collection = @collection || client.send(collection_name)
|
|
117
|
+
params.empty? ? collection.delete(id) : collection.delete(id, params.compact)
|
|
114
118
|
true
|
|
115
119
|
else
|
|
116
120
|
warn "Warning: Client does not respond to collection '#{collection_name}' for destroying entity."
|
|
@@ -129,10 +133,11 @@ module MOCO
|
|
|
129
133
|
# Check if the client responds to the collection method
|
|
130
134
|
if client.respond_to?(collection_name)
|
|
131
135
|
# Use the collection proxy to find the entity
|
|
132
|
-
reloaded = client.send(collection_name).find(id)
|
|
136
|
+
reloaded = (@collection || client.send(collection_name)).find(id)
|
|
133
137
|
|
|
134
138
|
# Update attributes with the reloaded data
|
|
135
139
|
@attributes = reloaded.attributes if reloaded
|
|
140
|
+
define_attribute_methods
|
|
136
141
|
else
|
|
137
142
|
warn "Warning: Client does not respond to collection '#{collection_name}' for reloading entity."
|
|
138
143
|
end
|
|
@@ -47,8 +47,21 @@ module MOCO
|
|
|
47
47
|
"account/catalog_services"
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
def items
|
|
51
|
+
CatalogServiceItemsProxy.new(client, self, :items, "CatalogServiceItem")
|
|
52
|
+
end
|
|
53
|
+
|
|
50
54
|
def to_s
|
|
51
55
|
name.to_s
|
|
52
56
|
end
|
|
53
57
|
end
|
|
58
|
+
|
|
59
|
+
# Catalog items are listed inside the parent, not at a collection endpoint.
|
|
60
|
+
class CatalogServiceItemsProxy < NestedCollectionProxy
|
|
61
|
+
def load_records
|
|
62
|
+
@records = wrap_response(parent.attributes.fetch(:items, []).map { |item| item.is_a?(BaseEntity) ? item.to_h : item })
|
|
63
|
+
@loaded = true
|
|
64
|
+
@records
|
|
65
|
+
end
|
|
66
|
+
end
|
|
54
67
|
end
|
|
@@ -59,6 +59,16 @@ module MOCO
|
|
|
59
59
|
# )
|
|
60
60
|
#
|
|
61
61
|
class Company < BaseEntity
|
|
62
|
+
def archive
|
|
63
|
+
client.put("companies/#{id}/archive")
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def unarchive
|
|
68
|
+
client.put("companies/#{id}/unarchive")
|
|
69
|
+
self
|
|
70
|
+
end
|
|
71
|
+
|
|
62
72
|
# Associations
|
|
63
73
|
def projects
|
|
64
74
|
has_many(:projects)
|
|
@@ -9,7 +9,7 @@ module MOCO
|
|
|
9
9
|
# date - String, "YYYY-MM-DD" invoice date
|
|
10
10
|
# due_date - String, "YYYY-MM-DD" payment due date
|
|
11
11
|
# title - String, invoice title (e.g., "Invoice")
|
|
12
|
-
#
|
|
12
|
+
# vat_code_id - Integer, sales VAT code ID (tax percentage is deprecated)
|
|
13
13
|
# currency - String, 3-letter code (e.g., "EUR")
|
|
14
14
|
# items - Array of Hashes, invoice line items (see below)
|
|
15
15
|
#
|
|
@@ -47,7 +47,7 @@ module MOCO
|
|
|
47
47
|
# date: "2024-01-15",
|
|
48
48
|
# due_date: "2024-02-15",
|
|
49
49
|
# title: "Invoice",
|
|
50
|
-
#
|
|
50
|
+
# vat_code_id: 123,
|
|
51
51
|
# currency: "EUR",
|
|
52
52
|
# items: [
|
|
53
53
|
# { type: "title", title: "Services January 2024" },
|
data/lib/moco/entities/offer.rb
CHANGED
|
@@ -91,6 +91,23 @@ module MOCO
|
|
|
91
91
|
MOCO::NestedCollectionProxy.new(client, self, :attachments, "OfferAttachment")
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
+
def confirmations
|
|
95
|
+
CollectionProxy.new(client, "offer_confirmations", "OfferConfirmation").where(offer_id: id)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def customer_approval
|
|
99
|
+
OfferApproval.new(client, client.get("offers/#{id}/customer_approval"))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def activate_customer_approval
|
|
103
|
+
OfferApproval.new(client, client.post("offers/#{id}/customer_approval/activate"))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def deactivate_customer_approval
|
|
107
|
+
client.post("offers/#{id}/customer_approval/deactivate")
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
94
111
|
# Associations
|
|
95
112
|
def company
|
|
96
113
|
association(:customer, "Company")
|
|
@@ -23,7 +23,7 @@ module MOCO
|
|
|
23
23
|
# approval = moco.get("offers/123/customer_approval")
|
|
24
24
|
#
|
|
25
25
|
# # Deactivate (revoke access)
|
|
26
|
-
# moco.
|
|
26
|
+
# moco.post("offers/123/customer_approval/deactivate")
|
|
27
27
|
#
|
|
28
28
|
# == Note:
|
|
29
29
|
# Check signed_at to determine if the customer has signed.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOCO
|
|
4
|
+
# Confirmation document for an accepted offer.
|
|
5
|
+
class OfferConfirmation < BaseEntity
|
|
6
|
+
def pdf(letter_paper_id: nil)
|
|
7
|
+
client.get("offer_confirmations/#{id}.pdf", { letter_paper_id: }.compact)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def offer
|
|
11
|
+
association(:offer)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def user
|
|
15
|
+
association(:user)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOCO
|
|
4
|
+
# Planned outgoing expense, optionally recurring.
|
|
5
|
+
class PlannedPurchase < BaseEntity
|
|
6
|
+
def company
|
|
7
|
+
association(:company)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def user
|
|
11
|
+
association(:user)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def purchase_budget
|
|
15
|
+
association(:purchase_budget)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -10,7 +10,7 @@ module MOCO
|
|
|
10
10
|
# "bank_transfer", "direct_debit", "credit_card",
|
|
11
11
|
# "paypal", "cash", "bank_transfer_swiss_qr_esr"
|
|
12
12
|
# items - Array of item hashes (at least one required):
|
|
13
|
-
# { title: "Item", total: 100.0,
|
|
13
|
+
# { title: "Item", total: 100.0, vat_code_id: 123, tax_included: true }
|
|
14
14
|
#
|
|
15
15
|
# == Optional attributes:
|
|
16
16
|
# title - String, purchase title (auto-generated from items if omitted)
|
|
@@ -31,7 +31,7 @@ module MOCO
|
|
|
31
31
|
# == Item attributes:
|
|
32
32
|
# title - String, item description
|
|
33
33
|
# total - Float, item total amount
|
|
34
|
-
#
|
|
34
|
+
# vat_code_id - Integer, purchase VAT code ID (tax percentage is deprecated)
|
|
35
35
|
# tax_included - Boolean, whether total includes tax
|
|
36
36
|
# category_id - Integer, purchase category ID
|
|
37
37
|
#
|
|
@@ -47,7 +47,7 @@ module MOCO
|
|
|
47
47
|
# payment_method: "bank_transfer",
|
|
48
48
|
# company_id: 456,
|
|
49
49
|
# items: [
|
|
50
|
-
# { title: "Office supplies", total: 119.0,
|
|
50
|
+
# { title: "Office supplies", total: 119.0, vat_code_id: 123, tax_included: true }
|
|
51
51
|
# ],
|
|
52
52
|
# tags: ["Office"]
|
|
53
53
|
# )
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module MOCO
|
|
4
4
|
# Represents a MOCO purchase budget (Ausgaben – Budgets)
|
|
5
|
-
#
|
|
5
|
+
# Budget tracking for expense categories
|
|
6
6
|
#
|
|
7
7
|
# == Read-only attributes:
|
|
8
8
|
# id, title, year, target, exhausted, remaining,
|
|
@@ -19,8 +19,7 @@ module MOCO
|
|
|
19
19
|
# end
|
|
20
20
|
#
|
|
21
21
|
# == Note:
|
|
22
|
-
# Purchase budgets
|
|
23
|
-
# This endpoint provides read-only access for tracking.
|
|
22
|
+
# Purchase budgets support create, update, delete, and annual target amounts.
|
|
24
23
|
#
|
|
25
24
|
class PurchaseBudget < BaseEntity
|
|
26
25
|
# Custom path since it's nested under purchases
|
|
@@ -28,6 +27,10 @@ module MOCO
|
|
|
28
27
|
"purchases/budgets"
|
|
29
28
|
end
|
|
30
29
|
|
|
30
|
+
def update_target(target_amount:, year: nil)
|
|
31
|
+
self.class.new(client, client.put("purchases/budgets/#{id}/target", { target_amount:, year: }.compact))
|
|
32
|
+
end
|
|
33
|
+
|
|
31
34
|
def to_s
|
|
32
35
|
"PurchaseBudget #{id}: #{title} (#{year})"
|
|
33
36
|
end
|
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module MOCO
|
|
4
4
|
# Represents a MOCO purchase draft
|
|
5
|
-
#
|
|
5
|
+
# Create drafts with a base64-encoded file; convert them in the MOCO interface.
|
|
6
6
|
#
|
|
7
|
-
# ==
|
|
7
|
+
# == Create attributes:
|
|
8
|
+
# title - String, required
|
|
9
|
+
# file - Optional Hash, { filename: "invoice.pdf", base64: "..." }
|
|
10
|
+
#
|
|
11
|
+
# == Response attributes:
|
|
8
12
|
# id, title, date, company (Hash), file_url,
|
|
9
13
|
# items (Array), created_at, updated_at
|
|
10
14
|
#
|
|
11
15
|
# == Note:
|
|
12
|
-
# Purchase drafts
|
|
13
|
-
# are uploaded/scanned in MOCO's inbox.
|
|
16
|
+
# Purchase drafts can be created through the API or MOCO's inbox.
|
|
14
17
|
# Convert to actual purchases via the MOCO interface.
|
|
15
18
|
#
|
|
16
19
|
class PurchaseDraft < BaseEntity
|
|
@@ -18,6 +21,10 @@ module MOCO
|
|
|
18
21
|
"purchases/drafts"
|
|
19
22
|
end
|
|
20
23
|
|
|
24
|
+
def pdf
|
|
25
|
+
client.get("purchases/drafts/#{id}.pdf")
|
|
26
|
+
end
|
|
27
|
+
|
|
21
28
|
def to_s
|
|
22
29
|
"Draft ##{id}"
|
|
23
30
|
end
|
|
@@ -43,6 +43,14 @@ module MOCO
|
|
|
43
43
|
# start_date and period cannot be modified after creation.
|
|
44
44
|
#
|
|
45
45
|
class RecurringExpense < BaseEntity
|
|
46
|
+
def recur(project_id: nil)
|
|
47
|
+
project_id ||= @collection.parent.id if @collection.is_a?(NestedCollectionProxy)
|
|
48
|
+
project_id ||= attributes[:project_id] || project&.id
|
|
49
|
+
raise ArgumentError, "project_id is required" unless project_id
|
|
50
|
+
|
|
51
|
+
client.post("projects/#{project_id}/recurring_expenses/#{id}/recur")
|
|
52
|
+
end
|
|
53
|
+
|
|
46
54
|
# Associations
|
|
47
55
|
def project
|
|
48
56
|
association(:project)
|
|
@@ -17,7 +17,8 @@ module MOCO
|
|
|
17
17
|
parent_type = ActiveSupport::Inflector.underscore(parent.class.name.split("::").last)
|
|
18
18
|
# Use simple tableized name, not entity_path (which might include 'projects/' prefix)
|
|
19
19
|
nested_path = ActiveSupport::Inflector.tableize(path_or_entity_name.to_s)
|
|
20
|
-
|
|
20
|
+
parent_path = parent.class.respond_to?(:entity_path) ? parent.class.entity_path : parent_type.pluralize
|
|
21
|
+
"#{parent_path}/#{parent.id}/#{nested_path}"
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
# Create a new entity in this nested collection
|
|
@@ -25,7 +26,7 @@ module MOCO
|
|
|
25
26
|
klass = entity_class
|
|
26
27
|
return nil unless klass && klass <= MOCO::BaseEntity
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
build_entity(client.post(@base_path, attributes))
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
# Delete all entities in this nested collection
|
data/lib/moco/version.rb
CHANGED
data/lib/moco.rb
CHANGED
|
@@ -41,9 +41,16 @@ require_relative "moco/entities/tagging"
|
|
|
41
41
|
require_relative "moco/entities/deal_category"
|
|
42
42
|
require_relative "moco/entities/project_group"
|
|
43
43
|
require_relative "moco/entities/profile"
|
|
44
|
+
require_relative "moco/entities/offer_confirmation"
|
|
45
|
+
require_relative "moco/entities/revenue_category"
|
|
46
|
+
require_relative "moco/entities/planned_purchase"
|
|
47
|
+
require_relative "moco/entities/todo"
|
|
48
|
+
require_relative "moco/entities/absence_request"
|
|
49
|
+
require_relative "moco/entities/approval"
|
|
44
50
|
|
|
45
51
|
# Account-level entities
|
|
46
52
|
require_relative "moco/entities/catalog_service"
|
|
53
|
+
require_relative "moco/entities/catalog_service_item"
|
|
47
54
|
require_relative "moco/entities/custom_property"
|
|
48
55
|
require_relative "moco/entities/expense_template"
|
|
49
56
|
require_relative "moco/entities/fixed_cost"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: moco-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Teal Bauer
|
|
@@ -78,9 +78,12 @@ files:
|
|
|
78
78
|
- lib/moco/collection_proxy.rb
|
|
79
79
|
- lib/moco/connection.rb
|
|
80
80
|
- lib/moco/entities.rb
|
|
81
|
+
- lib/moco/entities/absence_request.rb
|
|
81
82
|
- lib/moco/entities/activity.rb
|
|
83
|
+
- lib/moco/entities/approval.rb
|
|
82
84
|
- lib/moco/entities/base_entity.rb
|
|
83
85
|
- lib/moco/entities/catalog_service.rb
|
|
86
|
+
- lib/moco/entities/catalog_service_item.rb
|
|
84
87
|
- lib/moco/entities/comment.rb
|
|
85
88
|
- lib/moco/entities/company.rb
|
|
86
89
|
- lib/moco/entities/contact.rb
|
|
@@ -103,7 +106,9 @@ files:
|
|
|
103
106
|
- lib/moco/entities/offer.rb
|
|
104
107
|
- lib/moco/entities/offer_approval.rb
|
|
105
108
|
- lib/moco/entities/offer_attachment.rb
|
|
109
|
+
- lib/moco/entities/offer_confirmation.rb
|
|
106
110
|
- lib/moco/entities/payment_schedule.rb
|
|
111
|
+
- lib/moco/entities/planned_purchase.rb
|
|
107
112
|
- lib/moco/entities/planning_entry.rb
|
|
108
113
|
- lib/moco/entities/presence.rb
|
|
109
114
|
- lib/moco/entities/profile.rb
|
|
@@ -122,12 +127,14 @@ files:
|
|
|
122
127
|
- lib/moco/entities/reports/cashflow.rb
|
|
123
128
|
- lib/moco/entities/reports/finance.rb
|
|
124
129
|
- lib/moco/entities/reports/utilization.rb
|
|
130
|
+
- lib/moco/entities/revenue_category.rb
|
|
125
131
|
- lib/moco/entities/schedule.rb
|
|
126
132
|
- lib/moco/entities/session.rb
|
|
127
133
|
- lib/moco/entities/tag.rb
|
|
128
134
|
- lib/moco/entities/tagging.rb
|
|
129
135
|
- lib/moco/entities/task.rb
|
|
130
136
|
- lib/moco/entities/task_template.rb
|
|
137
|
+
- lib/moco/entities/todo.rb
|
|
131
138
|
- lib/moco/entities/unit.rb
|
|
132
139
|
- lib/moco/entities/user.rb
|
|
133
140
|
- lib/moco/entities/user_role.rb
|
|
@@ -165,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
165
172
|
- !ruby/object:Gem::Version
|
|
166
173
|
version: '0'
|
|
167
174
|
requirements: []
|
|
168
|
-
rubygems_version: 4.0.
|
|
175
|
+
rubygems_version: 4.0.20
|
|
169
176
|
specification_version: 4
|
|
170
177
|
summary: A Ruby Gem to interact with the MOCO API.
|
|
171
178
|
test_files: []
|