moco-ruby 1.1.0 → 1.3.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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/CHANGELOG.md +56 -1
  4. data/Gemfile.lock +45 -40
  5. data/README.md +98 -25
  6. data/lib/moco/client.rb +65 -0
  7. data/lib/moco/connection.rb +45 -22
  8. data/lib/moco/entities/activity.rb +31 -1
  9. data/lib/moco/entities/catalog_service.rb +54 -0
  10. data/lib/moco/entities/comment.rb +61 -0
  11. data/lib/moco/entities/company.rb +57 -2
  12. data/lib/moco/entities/contact.rb +56 -0
  13. data/lib/moco/entities/custom_property.rb +49 -0
  14. data/lib/moco/entities/deal.rb +38 -2
  15. data/lib/moco/entities/deal_category.rb +27 -0
  16. data/lib/moco/entities/employment.rb +55 -0
  17. data/lib/moco/entities/expense.rb +37 -2
  18. data/lib/moco/entities/expense_template.rb +39 -0
  19. data/lib/moco/entities/fixed_cost.rb +30 -0
  20. data/lib/moco/entities/holiday.rb +33 -2
  21. data/lib/moco/entities/hourly_rate.rb +33 -0
  22. data/lib/moco/entities/internal_hourly_rate.rb +32 -0
  23. data/lib/moco/entities/invoice.rb +70 -1
  24. data/lib/moco/entities/invoice_attachment.rb +34 -0
  25. data/lib/moco/entities/invoice_bookkeeping_export.rb +33 -0
  26. data/lib/moco/entities/invoice_payment.rb +51 -0
  27. data/lib/moco/entities/invoice_reminder.rb +51 -0
  28. data/lib/moco/entities/letter_paper.rb +23 -0
  29. data/lib/moco/entities/offer.rb +111 -0
  30. data/lib/moco/entities/offer_approval.rb +42 -0
  31. data/lib/moco/entities/offer_attachment.rb +34 -0
  32. data/lib/moco/entities/payment_schedule.rb +48 -0
  33. data/lib/moco/entities/planning_entry.rb +43 -2
  34. data/lib/moco/entities/presence.rb +34 -2
  35. data/lib/moco/entities/profile.rb +24 -0
  36. data/lib/moco/entities/project.rb +85 -10
  37. data/lib/moco/entities/project_contract.rb +50 -0
  38. data/lib/moco/entities/project_group.rb +38 -0
  39. data/lib/moco/entities/purchase.rb +90 -0
  40. data/lib/moco/entities/purchase_bookkeeping_export.rb +34 -0
  41. data/lib/moco/entities/purchase_budget.rb +47 -0
  42. data/lib/moco/entities/purchase_category.rb +38 -0
  43. data/lib/moco/entities/purchase_draft.rb +25 -0
  44. data/lib/moco/entities/purchase_payment.rb +51 -0
  45. data/lib/moco/entities/receipt.rb +55 -0
  46. data/lib/moco/entities/recurring_expense.rb +55 -0
  47. data/lib/moco/entities/reports/absences.rb +16 -0
  48. data/lib/moco/entities/reports/cashflow.rb +16 -0
  49. data/lib/moco/entities/reports/finance.rb +16 -0
  50. data/lib/moco/entities/reports/utilization.rb +16 -0
  51. data/lib/moco/entities/schedule.rb +39 -2
  52. data/lib/moco/entities/session.rb +58 -0
  53. data/lib/moco/entities/tag.rb +30 -0
  54. data/lib/moco/entities/tagging.rb +27 -0
  55. data/lib/moco/entities/task.rb +25 -2
  56. data/lib/moco/entities/task_template.rb +38 -0
  57. data/lib/moco/entities/unit.rb +36 -0
  58. data/lib/moco/entities/user.rb +50 -2
  59. data/lib/moco/entities/user_role.rb +29 -0
  60. data/lib/moco/entities/vat_code_purchase.rb +29 -0
  61. data/lib/moco/entities/vat_code_sale.rb +29 -0
  62. data/lib/moco/entities/web_hook.rb +32 -2
  63. data/lib/moco/entities/work_time_adjustment.rb +51 -0
  64. data/lib/moco/entities.rb +5 -5
  65. data/lib/moco/sync.rb +7 -7
  66. data/lib/moco/version.rb +1 -1
  67. data/lib/moco.rb +55 -1
  68. data/moco.gemspec +38 -0
  69. data/sync_activity.rb +1 -1
  70. metadata +51 -8
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents an attachment on a MOCO invoice
5
+ #
6
+ # == Required attributes for create:
7
+ # attachment - Hash with the following keys:
8
+ # filename - String, file name including extension (e.g., "appendix.pdf")
9
+ # base64 - String, base64-encoded file content
10
+ #
11
+ # == Read-only attributes:
12
+ # id, title, created_at, updated_at
13
+ #
14
+ # == Usage:
15
+ # invoice = moco.invoices.find(123)
16
+ # invoice.attachments.all
17
+ # invoice.attachments.create(
18
+ # attachment: {
19
+ # filename: "appendix.pdf",
20
+ # base64: Base64.strict_encode64(File.read("appendix.pdf"))
21
+ # }
22
+ # )
23
+ # invoice.attachments.find(42).destroy
24
+ #
25
+ # == Note:
26
+ # The API only supports list (GET), create (POST), and delete (DELETE).
27
+ # Update is not available - delete and re-upload to replace.
28
+ #
29
+ class InvoiceAttachment < BaseEntity
30
+ def to_s
31
+ title.to_s
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO invoice bookkeeping export (Buchhaltungsexporte)
5
+ # Exports invoice data for accounting systems
6
+ #
7
+ # == Read-only attributes:
8
+ # id, from, to, file_url, user (Hash),
9
+ # created_at, updated_at
10
+ #
11
+ # == Filtering:
12
+ # moco.invoice_bookkeeping_exports.all
13
+ # moco.invoice_bookkeeping_exports.where(from: "2024-01-01", to: "2024-01-31")
14
+ #
15
+ # == Note:
16
+ # Bookkeeping exports are generated via MOCO's finance interface.
17
+ # This endpoint provides read-only access to export records.
18
+ #
19
+ class InvoiceBookkeepingExport < BaseEntity
20
+ # Custom path since it's nested under invoices
21
+ def self.entity_path
22
+ "invoices/bookkeeping_exports"
23
+ end
24
+
25
+ def user
26
+ association(:user, "User")
27
+ end
28
+
29
+ def to_s
30
+ "InvoiceBookkeepingExport #{id} (#{from} - #{to})"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO invoice payment record
5
+ # (Rechnungen / Zahlungen) for tracking received payments
6
+ #
7
+ # == Required attributes for create:
8
+ # date - String, "YYYY-MM-DD" payment date
9
+ # paid_total - Float, amount received
10
+ #
11
+ # == Optional attributes:
12
+ # invoice_id - Integer, invoice being paid (required unless description set)
13
+ # currency - String, payment currency (e.g., "EUR")
14
+ # partially_paid - Boolean, mark as partial payment
15
+ # description - String, payment description (required if no invoice_id)
16
+ #
17
+ # == Read-only attributes:
18
+ # id, invoice (Hash), paid_total_in_account_currency,
19
+ # created_at, updated_at
20
+ #
21
+ # == Example:
22
+ # moco.invoice_payments.create(
23
+ # date: "2024-01-20",
24
+ # invoice_id: 456,
25
+ # paid_total: 5000.0,
26
+ # currency: "EUR"
27
+ # )
28
+ #
29
+ # == Bulk create:
30
+ # moco.post("invoices/payments/bulk", {
31
+ # bulk_data: [
32
+ # { date: "2024-01-20", invoice_id: 123, paid_total: 1000 },
33
+ # { date: "2024-01-21", invoice_id: 456, paid_total: 2000 }
34
+ # ]
35
+ # })
36
+ #
37
+ # == Filtering:
38
+ # moco.invoice_payments.where(invoice_id: 123)
39
+ # moco.invoice_payments.where(date_from: "2024-01-01", date_to: "2024-01-31")
40
+ #
41
+ class InvoicePayment < BaseEntity
42
+ # Associations
43
+ def invoice
44
+ association(:invoice)
45
+ end
46
+
47
+ def to_s
48
+ "Payment ##{id} (#{date})"
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO invoice reminder (Mahnung / Zahlungserinnerung)
5
+ #
6
+ # == Required attributes for create:
7
+ # invoice_id - Integer, overdue invoice ID
8
+ #
9
+ # == Optional attributes:
10
+ # title - String, reminder title (uses default if omitted)
11
+ # text - String, reminder message (uses default if omitted)
12
+ # fee - Float, late payment fee
13
+ # date - String, "YYYY-MM-DD" reminder date
14
+ # due_date - String, "YYYY-MM-DD" new payment due date
15
+ #
16
+ # == Read-only attributes:
17
+ # id, status ("created" or "sent"), file_url, invoice (Hash),
18
+ # created_at, updated_at
19
+ #
20
+ # == Example:
21
+ # moco.invoice_reminders.create(
22
+ # invoice_id: 456,
23
+ # title: "Payment Reminder",
24
+ # text: "Please remit payment within 14 days.",
25
+ # fee: 25.0,
26
+ # date: "2024-02-01",
27
+ # due_date: "2024-02-15"
28
+ # )
29
+ #
30
+ # == Send by email:
31
+ # moco.post("invoice_reminders/123/send_email", {
32
+ # emails_to: "customer@example.com",
33
+ # subject: "Payment Reminder",
34
+ # text: "Please see attached reminder."
35
+ # })
36
+ #
37
+ # == Filtering:
38
+ # moco.invoice_reminders.where(invoice_id: 456)
39
+ # moco.invoice_reminders.where(date_from: "2024-01-01", date_to: "2024-01-31")
40
+ #
41
+ class InvoiceReminder < BaseEntity
42
+ # Associations
43
+ def invoice
44
+ association(:invoice)
45
+ end
46
+
47
+ def to_s
48
+ "Reminder ##{id} (#{date})"
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO letter paper (letterhead template used on invoices/offers PDFs)
5
+ # Read-only listing of letterheads configured in the MOCO account.
6
+ #
7
+ # == Read-only attributes:
8
+ # id, name, active, template, file, created_at, updated_at
9
+ #
10
+ # == Usage:
11
+ # moco.letter_papers.all
12
+ #
13
+ # == Note:
14
+ # The API only exposes a list endpoint (GET /letter_papers).
15
+ # Use a letter paper's `id` as `letter_paper_id` when fetching
16
+ # invoice/offer PDFs (e.g. GET /invoices/{id}.pdf?letter_paper_id=...).
17
+ #
18
+ class LetterPaper < BaseEntity
19
+ def to_s
20
+ name.to_s
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO offer/quote
5
+ #
6
+ # == Required attributes for create:
7
+ # recipient_address - String, full address (use \r\n for line breaks)
8
+ # date - String, "YYYY-MM-DD" offer date
9
+ # due_date - String, "YYYY-MM-DD" valid until date
10
+ # title - String, offer title (e.g., "Offer - Website Relaunch")
11
+ # tax - Float, tax rate percentage (e.g., 19.0)
12
+ # items - Array of Hashes, offer line items (see below)
13
+ #
14
+ # == Item types (for items array):
15
+ # { type: "title", title: "Section Title" }
16
+ # { type: "description", description: "Description text" }
17
+ # { type: "item", title: "Service", quantity: 10, unit: "h", unit_price: 150.0 }
18
+ # { type: "item", title: "Fixed Fee", net_total: 500.0 } # lump sum
19
+ # { type: "subtotal" }
20
+ # { type: "separator" }
21
+ # { type: "page-break" }
22
+ #
23
+ # == Optional attributes:
24
+ # company_id - Integer, customer company ID (set from project if project_id provided)
25
+ # deal_id - Integer, associated deal ID
26
+ # project_id - Integer, associated project ID
27
+ # currency - String, 3-letter code (required if no company/deal/project)
28
+ # salutation - String, greeting text
29
+ # footer - String, footer text
30
+ # discount - Float, discount percentage
31
+ # contact_id - Integer, customer contact ID
32
+ # change_address - String, "offer" or "customer"
33
+ # tags - Array of Strings
34
+ #
35
+ # == Read-only attributes:
36
+ # id, identifier, status, net_total, gross_total, created_at, updated_at
37
+ #
38
+ # == Example:
39
+ # moco.offers.create(
40
+ # deal_id: 123456,
41
+ # recipient_address: "Acme Corp\r\n123 Main St",
42
+ # date: "2024-01-15",
43
+ # due_date: "2024-02-15",
44
+ # title: "Offer - Website Relaunch",
45
+ # tax: 19.0,
46
+ # items: [
47
+ # { type: "title", title: "Development Services" },
48
+ # { type: "item", title: "Frontend Development", quantity: 40, unit: "h", unit_price: 150.0 },
49
+ # { type: "item", title: "Backend Development", quantity: 60, unit: "h", unit_price: 150.0 }
50
+ # ]
51
+ # )
52
+ #
53
+ class Offer < BaseEntity
54
+ # Update the offer status
55
+ def update_status(status)
56
+ client.put("offers/#{id}/update_status", { status: })
57
+ self
58
+ end
59
+
60
+ # Get the offer as PDF
61
+ def pdf
62
+ client.get("offers/#{id}.pdf")
63
+ end
64
+
65
+ # Send the offer via email
66
+ def send_email(recipient:, subject:, text:, **options)
67
+ payload = {
68
+ recipient:,
69
+ subject:,
70
+ text:
71
+ }.merge(options)
72
+
73
+ client.post("offers/#{id}/send_email", payload)
74
+ self
75
+ end
76
+
77
+ # Assign offer to company, project, and/or deal
78
+ def assign(company_id: nil, project_id: nil, deal_id: nil)
79
+ payload = {}
80
+ payload[:company_id] = company_id if company_id
81
+ payload[:project_id] = project_id if project_id
82
+ payload[:deal_id] = deal_id if deal_id
83
+
84
+ client.put("offers/#{id}/assign", payload)
85
+ reload
86
+ end
87
+
88
+ # Fetches attachments for this offer as a NestedCollectionProxy.
89
+ # Supports .all, .find(id), .create(attachment: { filename:, base64: }), and .destroy.
90
+ def attachments
91
+ MOCO::NestedCollectionProxy.new(client, self, :attachments, "OfferAttachment")
92
+ end
93
+
94
+ # Associations
95
+ def company
96
+ association(:customer, "Company")
97
+ end
98
+
99
+ def project
100
+ association(:project)
101
+ end
102
+
103
+ def deal
104
+ association(:deal)
105
+ end
106
+
107
+ def to_s
108
+ "#{identifier} - #{title}"
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO offer customer approval (Kundenfreigabe)
5
+ # Allows customers to review and sign offers online
6
+ #
7
+ # == Read-only attributes:
8
+ # id, approval_url, offer_document_url, active,
9
+ # customer_full_name, customer_email, signature_url,
10
+ # signed_at, created_at, updated_at
11
+ #
12
+ # == Activation workflow:
13
+ # 1. Activate approval to generate shareable URL
14
+ # 2. Share offer_document_url with customer
15
+ # 3. Customer reviews and signs
16
+ # 4. Check signed_at to verify approval
17
+ #
18
+ # == Example:
19
+ # # Activate customer approval
20
+ # approval = moco.post("offers/123/customer_approval/activate")
21
+ #
22
+ # # Get approval status
23
+ # approval = moco.get("offers/123/customer_approval")
24
+ #
25
+ # # Deactivate (revoke access)
26
+ # moco.put("offers/123/customer_approval/deactivate")
27
+ #
28
+ # == Note:
29
+ # Check signed_at to determine if the customer has signed.
30
+ # Returns 404 if not yet activated.
31
+ #
32
+ class OfferApproval < BaseEntity
33
+ # Associations
34
+ def offer
35
+ association(:offer)
36
+ end
37
+
38
+ def to_s
39
+ "OfferApproval ##{id}"
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents an attachment on a MOCO offer
5
+ #
6
+ # == Required attributes for create:
7
+ # attachment - Hash with the following keys:
8
+ # filename - String, file name including extension (e.g., "appendix.pdf")
9
+ # base64 - String, base64-encoded file content
10
+ #
11
+ # == Read-only attributes:
12
+ # id, title, created_at, updated_at
13
+ #
14
+ # == Usage:
15
+ # offer = moco.offers.find(123)
16
+ # offer.attachments.all
17
+ # offer.attachments.create(
18
+ # attachment: {
19
+ # filename: "appendix.pdf",
20
+ # base64: Base64.strict_encode64(File.read("appendix.pdf"))
21
+ # }
22
+ # )
23
+ # offer.attachments.find(42).destroy
24
+ #
25
+ # == Note:
26
+ # The API only supports list (GET), create (POST), and delete (DELETE).
27
+ # Update is not available - delete and re-upload to replace.
28
+ #
29
+ class OfferAttachment < BaseEntity
30
+ def to_s
31
+ title.to_s
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents a MOCO project payment schedule entry
5
+ # (Geplante Abrechnungen) for fixed-price project milestones
6
+ #
7
+ # == Required attributes for create:
8
+ # net_total - Float, payment amount
9
+ # date - String, "YYYY-MM-DD" scheduled payment date
10
+ #
11
+ # == Optional attributes:
12
+ # title - String, milestone name (e.g., "First installment")
13
+ # description - String, milestone details (HTML allowed)
14
+ # checked - Boolean, mark as completed
15
+ #
16
+ # == Read-only attributes:
17
+ # id, project (Hash), billed, created_at, updated_at
18
+ #
19
+ # == Access methods:
20
+ # # All payment schedules across projects
21
+ # moco.payment_schedules.all
22
+ #
23
+ # # Filter by project
24
+ # moco.payment_schedules.where(project_id: 123)
25
+ #
26
+ # == Example:
27
+ # moco.post("projects/123/payment_schedules", {
28
+ # net_total: 5000.0,
29
+ # date: "2024-03-15",
30
+ # title: "Design Phase Complete"
31
+ # })
32
+ #
33
+ # == Filtering:
34
+ # moco.payment_schedules.where(from: "2024-01-01", to: "2024-12-31")
35
+ # moco.payment_schedules.where(checked: false) # unpaid only
36
+ # moco.payment_schedules.where(company_id: 456)
37
+ #
38
+ class PaymentSchedule < BaseEntity
39
+ # Associations
40
+ def project
41
+ association(:project)
42
+ end
43
+
44
+ def to_s
45
+ "PaymentSchedule ##{id} (#{date})"
46
+ end
47
+ end
48
+ end
@@ -1,8 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MOCO
4
- # Represents a MOCO planning entry
5
- # Provides methods for planning entry-specific associations
4
+ # Represents a MOCO planning entry (resource scheduling)
5
+ # For absences, use Schedule instead
6
+ #
7
+ # == Required attributes for create:
8
+ # project_id OR deal_id - Integer, must provide exactly one
9
+ # starts_on - String, "YYYY-MM-DD" start date
10
+ # ends_on - String, "YYYY-MM-DD" end date
11
+ # hours_per_day - Float/Integer, planned hours per day
12
+ #
13
+ # == Optional attributes:
14
+ # user_id - Integer, user ID (default: current user)
15
+ # task_id - Integer, task ID (only with project_id)
16
+ # comment - String, notes about the planning
17
+ # symbol - Integer, 1-10 for visual indicator:
18
+ # 1=home, 2=building, 3=car, 4=graduation cap, 5=cocktail,
19
+ # 6=bells, 7=baby carriage, 8=users, 9=moon, 10=info circle
20
+ # tentative - Boolean, true if this is a blocker/tentative
21
+ #
22
+ # == Read-only attributes:
23
+ # id, color, read_only, user (Hash), project (Hash), deal (Hash),
24
+ # series_id, series_repeat, created_at, updated_at
25
+ #
26
+ # == Example:
27
+ # # Plan user on project for a week
28
+ # moco.planning_entries.create(
29
+ # project_id: 123,
30
+ # task_id: 456,
31
+ # user_id: 789,
32
+ # starts_on: "2024-01-15",
33
+ # ends_on: "2024-01-19",
34
+ # hours_per_day: 6,
35
+ # comment: "Sprint planning"
36
+ # )
37
+ #
38
+ # # Plan user on deal (pre-sales)
39
+ # moco.planning_entries.create(
40
+ # deal_id: 123,
41
+ # starts_on: "2024-01-22",
42
+ # ends_on: "2024-01-22",
43
+ # hours_per_day: 4,
44
+ # tentative: true
45
+ # )
46
+ #
6
47
  class PlanningEntry < BaseEntity
7
48
  # Associations
8
49
  def user
@@ -1,8 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MOCO
4
- # Represents a MOCO presence entry
5
- # Provides methods for presence-specific operations and associations
4
+ # Represents a MOCO presence entry (work time tracking)
5
+ #
6
+ # == Required attributes for create:
7
+ # date - String, "YYYY-MM-DD" date
8
+ # from - String, "HH:MM" start time (e.g., "08:00")
9
+ #
10
+ # == Optional attributes:
11
+ # to - String, "HH:MM" end time (e.g., "17:00"), can be blank for open entry
12
+ # is_home_office - Boolean, whether working from home (default: false)
13
+ #
14
+ # == Read-only attributes:
15
+ # id, user (Hash), created_at, updated_at
16
+ #
17
+ # == Class methods:
18
+ # Presence.touch(client) - Clock in/out (creates or closes presence)
19
+ #
20
+ # == Example:
21
+ # # Log work time
22
+ # moco.presences.create(
23
+ # date: "2024-01-15",
24
+ # from: "09:00",
25
+ # to: "17:30"
26
+ # )
27
+ #
28
+ # # Start work (open-ended)
29
+ # moco.presences.create(
30
+ # date: "2024-01-16",
31
+ # from: "08:30",
32
+ # is_home_office: true
33
+ # )
34
+ #
35
+ # # Clock in/out via touch
36
+ # MOCO::Presence.touch(moco)
37
+ #
6
38
  class Presence < BaseEntity
7
39
  # Define the specific API path for this entity as a class method
8
40
  def self.entity_path
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MOCO
4
+ # Represents the current API user's profile
5
+ # Read-only singleton endpoint for the authenticated user
6
+ #
7
+ # == Read-only attributes:
8
+ # id, firstname, lastname, email, unit (Hash),
9
+ # created_at, updated_at
10
+ #
11
+ # == Usage:
12
+ # profile = moco.profile.get
13
+ # puts "Logged in as: #{profile.firstname} #{profile.lastname}"
14
+ #
15
+ # == Note:
16
+ # This returns information about the user who owns the API key.
17
+ # For other user information, use moco.users.
18
+ #
19
+ class Profile < BaseEntity
20
+ def to_s
21
+ "#{firstname} #{lastname}"
22
+ end
23
+ end
24
+ end
@@ -1,6 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MOCO
4
+ # Represents a MOCO project
5
+ #
6
+ # == Required attributes for create:
7
+ # name - String, project name (e.g., "Website Relaunch")
8
+ # currency - String, 3-letter currency code (e.g., "EUR", "USD", "CHF")
9
+ # start_date - String, "YYYY-MM-DD" format (required, must be 1st of month if retainer)
10
+ # finish_date - String, "YYYY-MM-DD" format (required, must be last of month if retainer)
11
+ # fixed_price - Boolean, true for fixed-price projects
12
+ # retainer - Boolean, true for retainer/recurring projects
13
+ # leader_id - Integer, user ID of the project leader
14
+ # customer_id - Integer, company ID of the customer
15
+ # identifier - String, project identifier (e.g., "P-123") - only required if manual numbering
16
+ #
17
+ # == Optional attributes:
18
+ # co_leader_id - Integer, user ID of co-leader
19
+ # deal_id - Integer, associated deal ID
20
+ # project_group_id - Integer, project group ID
21
+ # contact_id - Integer, primary contact ID
22
+ # secondary_contact_id - Integer, secondary contact ID
23
+ # billing_contact_id - Integer, billing contact ID
24
+ # billing_address - String, billing address (multiline with \n)
25
+ # billing_email_to - String, email for invoices
26
+ # billing_email_cc - String, CC email for invoices
27
+ # billing_notes - String, notes for billing
28
+ # billing_variant - String, "project", "task", or "user" (default: "project")
29
+ # setting_include_time_report - Boolean, include time report with invoices
30
+ # hourly_rate - Float/Integer, hourly rate (meaning depends on billing_variant)
31
+ # budget - Float/Integer, total budget
32
+ # budget_monthly - Float/Integer, monthly budget (required if retainer: true)
33
+ # budget_expenses - Float/Integer, expenses budget
34
+ # tags - Array of Strings, e.g., ["Print", "Digital"]
35
+ # custom_properties - Hash, e.g., {"PO-Number": "123-ABC"}
36
+ # info - String, additional info
37
+ #
38
+ # == Read-only attributes (returned by API):
39
+ # id, active, color, customer (Hash), leader (Hash), co_leader (Hash),
40
+ # deal (Hash), tasks (Array), contracts (Array), project_group (Hash),
41
+ # created_at, updated_at
42
+ #
43
+ # == Example:
44
+ # moco.projects.create(
45
+ # name: "Website Relaunch",
46
+ # currency: "EUR",
47
+ # start_date: "2024-01-01",
48
+ # finish_date: "2024-12-31",
49
+ # fixed_price: false,
50
+ # retainer: false,
51
+ # leader_id: 123456,
52
+ # customer_id: 789012,
53
+ # budget: 50000
54
+ # )
55
+ #
4
56
  class Project < BaseEntity
5
57
  def customer
6
58
  # Use the association method to fetch the customer
@@ -30,19 +82,42 @@ module MOCO
30
82
  MOCO::NestedCollectionProxy.new(client, self, :expenses, "Expense")
31
83
  end
32
84
 
33
- # Fetches tasks associated with this project.
85
+ # Fetches tasks associated with this project via the API.
86
+ # Returns a NestedCollectionProxy for lazy loading and CRUD operations.
34
87
  def tasks
35
- # If tasks are already embedded in the attributes (e.g., from projects.assigned),
36
- # return them directly instead of making a new API call
37
- embedded_tasks = attributes[:tasks]
38
- if embedded_tasks.is_a?(Array) && embedded_tasks.all? { |t| t.is_a?(MOCO::Task) }
39
- return embedded_tasks
88
+ MOCO::NestedCollectionProxy.new(client, self, :tasks, "Task")
89
+ end
90
+
91
+ # Returns embedded tasks from the projects/assigned response, or nil.
92
+ # These have fewer fields than the full API response but avoid an
93
+ # extra API call, useful for limited-permission accounts.
94
+ def embedded_tasks
95
+ embedded = attributes[:tasks]
96
+ if embedded.is_a?(Array) && embedded.all? { |t| t.is_a?(MOCO::Task) }
97
+ embedded
98
+ else
99
+ []
40
100
  end
101
+ end
41
102
 
42
- # Otherwise, create a proxy for fetching tasks via API
43
- # Don't cache the proxy - create a fresh one each time
44
- # This ensures we get fresh data when tasks are created/updated/deleted
45
- MOCO::NestedCollectionProxy.new(client, self, :tasks, "Task")
103
+ # Fetches contracts associated with this project.
104
+ def contracts
105
+ MOCO::NestedCollectionProxy.new(client, self, :contracts, "ProjectContract")
106
+ end
107
+
108
+ # Fetches payment schedules associated with this project.
109
+ def payment_schedules
110
+ MOCO::NestedCollectionProxy.new(client, self, :payment_schedules, "PaymentSchedule")
111
+ end
112
+
113
+ # Fetches recurring expenses associated with this project.
114
+ def recurring_expenses
115
+ MOCO::NestedCollectionProxy.new(client, self, :recurring_expenses, "RecurringExpense")
116
+ end
117
+
118
+ # Get the project group
119
+ def project_group
120
+ association(:project_group)
46
121
  end
47
122
 
48
123
  def to_s