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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a32219e92cf0b5cc11f885486ec9a0afce63d39d2a2de95f3d5225b5521020e5
4
- data.tar.gz: fa20563a9ff63d9b412540b99beecdab249ee7dca31608d23a47a82926062f27
3
+ metadata.gz: 05ee5448c599c6d3aa08e1b3ee7f39937fdd83504e1448058101442f2ea0b9f8
4
+ data.tar.gz: 66fef5ab73ebc81eaa8a8559ec4fd492fccf75e301ff8e0c7de7e3f0a1442caa
5
5
  SHA512:
6
- metadata.gz: 63995cff5c2e36f43619a43e04208460d8aa3a899f06a8b4e0acc673655af550ac40f472519096a8da21d73d850705a4d4a10d788287eb6cac1a461d614c4139
7
- data.tar.gz: 7be74a0104d6bfffd3d3bb93f6658db539ec1727e108795f69a63c1b38447d0876eb21187c0557936fa0b70d7457a4150bccd9eed3ba04e8df6f3d32c59179ef
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- moco-ruby (1.3.0)
4
+ moco-ruby (1.4.0)
5
5
  activesupport (>= 7.0)
6
6
  faraday (>= 2.0)
7
7
  fuzzy_match (~> 2.1.0)
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
- klass.new(client, client.post(@base_path, attributes))
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
- klass.new(client, client.put("#{@base_path}/#{id}", attributes))
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| klass.new(client, item_hash) if item_hash.is_a?(Hash) }.compact
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
- [klass.new(client, response_body)]
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."
@@ -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
- f.request :authorization, "Token", "token=#{@api_key}"
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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Staff absence request for vacation, sickness, or other absences.
5
+ class AbsenceRequest < BaseEntity
6
+ def self.entity_path
7
+ "users/absence_requests"
8
+ end
9
+ end
10
+ 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, attributes)
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
- send("#{key}=", value) if respond_to?("#{key}=")
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).delete(id)
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Line item within an account catalog service.
5
+ class CatalogServiceItem < BaseEntity
6
+ end
7
+ 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
- # tax - Float, tax rate percentage (e.g., 19.0)
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
- # tax: 19.0,
50
+ # vat_code_id: 123,
51
51
  # currency: "EUR",
52
52
  # items: [
53
53
  # { type: "title", title: "Services January 2024" },
@@ -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.put("offers/123/customer_approval/deactivate")
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, tax: 7.7, tax_included: true }
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
- # tax - Float, tax percentage (e.g., 7.7)
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, tax: 19.0, tax_included: true }
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
- # Read-only budget tracking for expense categories
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 are configured in MOCO's admin interface.
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
- # Auto-created draft purchases from document scanning
5
+ # Create drafts with a base64-encoded file; convert them in the MOCO interface.
6
6
  #
7
- # == Read-only attributes:
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 are created automatically when documents
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)
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Revenue account category for tasks and additional services.
5
+ class RevenueCategory < BaseEntity
6
+ def to_s
7
+ name
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Read-only to-do entry.
5
+ class Todo < BaseEntity
6
+ end
7
+ end
@@ -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
- "#{parent_type.pluralize}/#{parent.id}/#{nested_path}"
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
- klass.new(client, client.post(@base_path, attributes))
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MOCO
4
- VERSION = "1.3.0"
4
+ VERSION = "1.4.0"
5
5
  end
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.3.0
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.10
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: []