moco-ruby 1.0.0 → 1.2.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 +36 -1
- data/Gemfile.lock +47 -43
- data/README.md +63 -24
- data/copy_project.rb +337 -0
- data/lib/moco/client.rb +58 -0
- data/lib/moco/connection.rb +45 -22
- data/lib/moco/entities/activity.rb +31 -1
- data/lib/moco/entities/catalog_service.rb +54 -0
- data/lib/moco/entities/comment.rb +61 -0
- data/lib/moco/entities/company.rb +57 -2
- data/lib/moco/entities/contact.rb +56 -0
- data/lib/moco/entities/custom_property.rb +49 -0
- data/lib/moco/entities/deal.rb +38 -2
- data/lib/moco/entities/deal_category.rb +27 -0
- data/lib/moco/entities/employment.rb +55 -0
- data/lib/moco/entities/expense.rb +37 -2
- data/lib/moco/entities/expense_template.rb +39 -0
- data/lib/moco/entities/fixed_cost.rb +30 -0
- data/lib/moco/entities/holiday.rb +33 -2
- data/lib/moco/entities/hourly_rate.rb +33 -0
- data/lib/moco/entities/internal_hourly_rate.rb +32 -0
- data/lib/moco/entities/invoice.rb +81 -1
- data/lib/moco/entities/invoice_bookkeeping_export.rb +33 -0
- data/lib/moco/entities/invoice_payment.rb +51 -0
- data/lib/moco/entities/invoice_reminder.rb +51 -0
- data/lib/moco/entities/offer.rb +122 -0
- data/lib/moco/entities/offer_approval.rb +42 -0
- data/lib/moco/entities/payment_schedule.rb +48 -0
- data/lib/moco/entities/planning_entry.rb +43 -2
- data/lib/moco/entities/presence.rb +34 -2
- data/lib/moco/entities/profile.rb +24 -0
- data/lib/moco/entities/project.rb +76 -2
- data/lib/moco/entities/project_contract.rb +50 -0
- data/lib/moco/entities/project_group.rb +38 -0
- data/lib/moco/entities/purchase.rb +90 -0
- data/lib/moco/entities/purchase_bookkeeping_export.rb +34 -0
- data/lib/moco/entities/purchase_budget.rb +47 -0
- data/lib/moco/entities/purchase_category.rb +38 -0
- data/lib/moco/entities/purchase_draft.rb +25 -0
- data/lib/moco/entities/purchase_payment.rb +51 -0
- data/lib/moco/entities/receipt.rb +55 -0
- data/lib/moco/entities/recurring_expense.rb +55 -0
- data/lib/moco/entities/reports/absences.rb +16 -0
- data/lib/moco/entities/reports/cashflow.rb +16 -0
- data/lib/moco/entities/reports/finance.rb +16 -0
- data/lib/moco/entities/reports/utilization.rb +16 -0
- data/lib/moco/entities/schedule.rb +39 -2
- data/lib/moco/entities/tag.rb +30 -0
- data/lib/moco/entities/tagging.rb +27 -0
- data/lib/moco/entities/task.rb +25 -2
- data/lib/moco/entities/task_template.rb +38 -0
- data/lib/moco/entities/unit.rb +36 -0
- data/lib/moco/entities/user.rb +50 -2
- data/lib/moco/entities/user_role.rb +29 -0
- data/lib/moco/entities/vat_code_purchase.rb +29 -0
- data/lib/moco/entities/vat_code_sale.rb +29 -0
- data/lib/moco/entities/web_hook.rb +32 -2
- data/lib/moco/entities/work_time_adjustment.rb +51 -0
- data/lib/moco/sync.rb +112 -6
- data/lib/moco/version.rb +1 -1
- data/lib/moco-ruby.rb +6 -0
- data/lib/moco.rb +51 -1
- data/moco.gemspec +5 -3
- data/sync_activity.rb +8 -2
- metadata +54 -14
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOCO
|
|
4
|
+
module Reports
|
|
5
|
+
# Represents a MOCO finance report (read-only)
|
|
6
|
+
class Finance < BaseEntity
|
|
7
|
+
def self.entity_path
|
|
8
|
+
"report/finance"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_s
|
|
12
|
+
"FinanceReport"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOCO
|
|
4
|
+
module Reports
|
|
5
|
+
# Represents a MOCO utilization report (read-only)
|
|
6
|
+
class Utilization < BaseEntity
|
|
7
|
+
def self.entity_path
|
|
8
|
+
"report/utilization"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_s
|
|
12
|
+
"UtilizationReport"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -1,8 +1,45 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module MOCO
|
|
4
|
-
# Represents a MOCO schedule entry
|
|
5
|
-
#
|
|
4
|
+
# Represents a MOCO schedule entry (absence/time-off)
|
|
5
|
+
# Note: For project planning, use PlanningEntry instead
|
|
6
|
+
#
|
|
7
|
+
# == Required attributes for create:
|
|
8
|
+
# date - String, "YYYY-MM-DD" date of absence
|
|
9
|
+
# absence_code - Integer, type of absence:
|
|
10
|
+
# 1 = unplannable absence
|
|
11
|
+
# 2 = public holiday
|
|
12
|
+
# 3 = sick day
|
|
13
|
+
# 4 = holiday/vacation
|
|
14
|
+
# 5 = other absence
|
|
15
|
+
#
|
|
16
|
+
# == Optional attributes:
|
|
17
|
+
# user_id - Integer, user ID (default: current user)
|
|
18
|
+
# am - Boolean, morning absence (default: true)
|
|
19
|
+
# pm - Boolean, afternoon absence (default: true)
|
|
20
|
+
# comment - String, comment/note
|
|
21
|
+
# symbol - Integer, 1-6 for half day visualization
|
|
22
|
+
#
|
|
23
|
+
# == Read-only attributes:
|
|
24
|
+
# id, assignment (Hash), user (Hash), created_at, updated_at
|
|
25
|
+
#
|
|
26
|
+
# == Example:
|
|
27
|
+
# # Full day vacation
|
|
28
|
+
# moco.schedules.create(
|
|
29
|
+
# date: "2024-01-15",
|
|
30
|
+
# absence_code: 4,
|
|
31
|
+
# user_id: 123,
|
|
32
|
+
# comment: "Annual leave"
|
|
33
|
+
# )
|
|
34
|
+
#
|
|
35
|
+
# # Half day sick (morning only)
|
|
36
|
+
# moco.schedules.create(
|
|
37
|
+
# date: "2024-01-16",
|
|
38
|
+
# absence_code: 3,
|
|
39
|
+
# am: true,
|
|
40
|
+
# pm: false
|
|
41
|
+
# )
|
|
42
|
+
#
|
|
6
43
|
class Schedule < BaseEntity
|
|
7
44
|
# Associations
|
|
8
45
|
def user
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOCO
|
|
4
|
+
# Represents a MOCO tag/label
|
|
5
|
+
#
|
|
6
|
+
# == Required attributes for create:
|
|
7
|
+
# name - String, tag name (e.g., "Important")
|
|
8
|
+
# context - String, entity type this tag applies to:
|
|
9
|
+
# "Project", "Contact", "Company", "Deal", "Offer",
|
|
10
|
+
# "Invoice", "Purchase", "User"
|
|
11
|
+
#
|
|
12
|
+
# == Read-only attributes:
|
|
13
|
+
# id, created_at, updated_at
|
|
14
|
+
#
|
|
15
|
+
# == Example:
|
|
16
|
+
# moco.tags.create(
|
|
17
|
+
# name: "Priority",
|
|
18
|
+
# context: "Project"
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
# == Note:
|
|
22
|
+
# To apply tags to entities, use the `tags` attribute when
|
|
23
|
+
# creating/updating the entity: { tags: ["Tag1", "Tag2"] }
|
|
24
|
+
#
|
|
25
|
+
class Tag < BaseEntity
|
|
26
|
+
def to_s
|
|
27
|
+
name.to_s
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOCO
|
|
4
|
+
# Represents a MOCO tagging (association between a tag and an entity)
|
|
5
|
+
# This is a read-only entity representing tag assignments.
|
|
6
|
+
#
|
|
7
|
+
# == Read-only attributes:
|
|
8
|
+
# id, entity_id, entity_type, tag (Hash), created_at, updated_at
|
|
9
|
+
#
|
|
10
|
+
# == Note:
|
|
11
|
+
# To apply tags to entities, use the `tags` attribute when
|
|
12
|
+
# creating/updating the entity directly:
|
|
13
|
+
# moco.projects.update(123, tags: ["Priority", "Important"])
|
|
14
|
+
#
|
|
15
|
+
# See also: Tag entity for managing available tags.
|
|
16
|
+
#
|
|
17
|
+
class Tagging < BaseEntity
|
|
18
|
+
# Associations
|
|
19
|
+
def tag
|
|
20
|
+
association(:tag)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_s
|
|
24
|
+
"Tagging ##{id}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/moco/entities/task.rb
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module MOCO
|
|
4
|
-
# Represents a MOCO task
|
|
5
|
-
#
|
|
4
|
+
# Represents a MOCO task (project service/activity type)
|
|
5
|
+
# Tasks are nested under projects: project.tasks.create(...)
|
|
6
|
+
#
|
|
7
|
+
# == Required attributes for create:
|
|
8
|
+
# name - String, task name (e.g., "Development", "Design / UX")
|
|
9
|
+
#
|
|
10
|
+
# == Optional attributes:
|
|
11
|
+
# billable - Boolean, whether time on this task is billable
|
|
12
|
+
# active - Boolean, whether task is active
|
|
13
|
+
# budget - Float/Integer, budget in hours or currency
|
|
14
|
+
# hourly_rate - Float/Integer, rate for this task (used if project billing_variant is "task")
|
|
15
|
+
# description - String, task description
|
|
16
|
+
#
|
|
17
|
+
# == Read-only attributes (returned by API):
|
|
18
|
+
# id, created_at, updated_at
|
|
19
|
+
#
|
|
20
|
+
# == Example:
|
|
21
|
+
# project = moco.projects.find(123)
|
|
22
|
+
# project.tasks.create(
|
|
23
|
+
# name: "Development",
|
|
24
|
+
# billable: true,
|
|
25
|
+
# hourly_rate: 150,
|
|
26
|
+
# budget: 100
|
|
27
|
+
# )
|
|
28
|
+
#
|
|
6
29
|
class Task < BaseEntity
|
|
7
30
|
# Associations
|
|
8
31
|
def project
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOCO
|
|
4
|
+
# Represents a MOCO task template (Standardleistungen)
|
|
5
|
+
# Pre-defined task types for projects
|
|
6
|
+
#
|
|
7
|
+
# == Required attributes for create:
|
|
8
|
+
# name - String, task name (e.g., "Development", "Design")
|
|
9
|
+
#
|
|
10
|
+
# == Optional attributes:
|
|
11
|
+
# description - String, task description
|
|
12
|
+
# revenue_category_id - Integer, revenue category for invoicing
|
|
13
|
+
# billable - Boolean, whether tasks are billable by default
|
|
14
|
+
# project_default - Boolean, auto-add to new projects
|
|
15
|
+
# index - Integer, display order (e.g., 10, 20, 30)
|
|
16
|
+
#
|
|
17
|
+
# == Read-only attributes:
|
|
18
|
+
# id, revenue_category (Hash), created_at, updated_at
|
|
19
|
+
#
|
|
20
|
+
# == Example:
|
|
21
|
+
# moco.task_templates.create(
|
|
22
|
+
# name: "Backend Development",
|
|
23
|
+
# description: "Server-side programming",
|
|
24
|
+
# billable: true,
|
|
25
|
+
# project_default: true,
|
|
26
|
+
# index: 10
|
|
27
|
+
# )
|
|
28
|
+
#
|
|
29
|
+
class TaskTemplate < BaseEntity
|
|
30
|
+
def self.entity_path
|
|
31
|
+
"account/task_templates"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_s
|
|
35
|
+
name.to_s
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOCO
|
|
4
|
+
# Represents a MOCO unit/team (Teams)
|
|
5
|
+
#
|
|
6
|
+
# == Required attributes for create:
|
|
7
|
+
# name - String, team name (e.g., "Development Team")
|
|
8
|
+
#
|
|
9
|
+
# == Read-only attributes:
|
|
10
|
+
# id, users (Array of user hashes), created_at, updated_at
|
|
11
|
+
#
|
|
12
|
+
# == Example:
|
|
13
|
+
# # Create a new team
|
|
14
|
+
# moco.units.create(name: "Marketing Team")
|
|
15
|
+
#
|
|
16
|
+
# # Get users in a team
|
|
17
|
+
# team = moco.units.find(123)
|
|
18
|
+
# team.users # => Array of User objects
|
|
19
|
+
#
|
|
20
|
+
# == Note:
|
|
21
|
+
# To assign users to a team, update the user with unit_id:
|
|
22
|
+
# moco.users.update(user_id, unit_id: team.id)
|
|
23
|
+
#
|
|
24
|
+
# Deleting a unit is only possible if no users are assigned to it.
|
|
25
|
+
#
|
|
26
|
+
class Unit < BaseEntity
|
|
27
|
+
# Get users belonging to this unit
|
|
28
|
+
def users
|
|
29
|
+
has_many(:users, :unit_id)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_s
|
|
33
|
+
name.to_s
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/moco/entities/user.rb
CHANGED
|
@@ -1,8 +1,44 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module MOCO
|
|
4
|
-
# Represents a MOCO user
|
|
5
|
-
#
|
|
4
|
+
# Represents a MOCO user (staff member)
|
|
5
|
+
#
|
|
6
|
+
# == Required attributes for create:
|
|
7
|
+
# firstname - String, first name
|
|
8
|
+
# lastname - String, last name
|
|
9
|
+
# email - String, email address (used for login)
|
|
10
|
+
# unit_id - Integer, team/unit ID
|
|
11
|
+
#
|
|
12
|
+
# == Optional attributes:
|
|
13
|
+
# password - String, initial password (if not set, user gets welcome email)
|
|
14
|
+
# role_id - Integer, permission role ID
|
|
15
|
+
# active - Boolean, whether user is active
|
|
16
|
+
# external - Boolean, true for contractors/external staff
|
|
17
|
+
# language - String, one of: "de", "de-AT", "de-CH", "en", "it", "fr"
|
|
18
|
+
# mobile_phone - String, mobile phone number
|
|
19
|
+
# work_phone - String, work phone number
|
|
20
|
+
# home_address - String, home address (use \n for line breaks)
|
|
21
|
+
# bday - String, birthday "YYYY-MM-DD"
|
|
22
|
+
# iban - String, bank account IBAN
|
|
23
|
+
# tags - Array of Strings, e.g., ["Developer", "Remote"]
|
|
24
|
+
# custom_properties - Hash, e.g., {"Start Date": "2024-01-01"}
|
|
25
|
+
# info - String, additional notes
|
|
26
|
+
# welcome_email - Boolean, send welcome email (default: true if no password)
|
|
27
|
+
# avatar - Hash, { filename: "photo.jpg", base64: "..." }
|
|
28
|
+
#
|
|
29
|
+
# == Read-only attributes:
|
|
30
|
+
# id, avatar_url, unit (Hash), role (Hash), created_at, updated_at
|
|
31
|
+
#
|
|
32
|
+
# == Example:
|
|
33
|
+
# moco.users.create(
|
|
34
|
+
# firstname: "John",
|
|
35
|
+
# lastname: "Doe",
|
|
36
|
+
# email: "john.doe@company.com",
|
|
37
|
+
# unit_id: 123,
|
|
38
|
+
# language: "en",
|
|
39
|
+
# tags: ["Developer"]
|
|
40
|
+
# )
|
|
41
|
+
#
|
|
6
42
|
class User < BaseEntity
|
|
7
43
|
# Instance methods for user-specific operations
|
|
8
44
|
def performance_report
|
|
@@ -22,6 +58,18 @@ module MOCO
|
|
|
22
58
|
has_many(:holidays)
|
|
23
59
|
end
|
|
24
60
|
|
|
61
|
+
def employments
|
|
62
|
+
has_many(:employments)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def work_time_adjustments
|
|
66
|
+
has_many(:work_time_adjustments)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def unit
|
|
70
|
+
association(:unit)
|
|
71
|
+
end
|
|
72
|
+
|
|
25
73
|
def full_name
|
|
26
74
|
"#{firstname} #{lastname}"
|
|
27
75
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOCO
|
|
4
|
+
# Represents a MOCO user permission role
|
|
5
|
+
# Read-only list of available permission roles
|
|
6
|
+
#
|
|
7
|
+
# == Read-only attributes:
|
|
8
|
+
# id, name, created_at, updated_at
|
|
9
|
+
#
|
|
10
|
+
# == Common roles:
|
|
11
|
+
# - Admin
|
|
12
|
+
# - Manager
|
|
13
|
+
# - Coworker
|
|
14
|
+
# - etc.
|
|
15
|
+
#
|
|
16
|
+
# == Note:
|
|
17
|
+
# Permission roles are configured in MOCO's admin interface.
|
|
18
|
+
# Use role_id when creating/updating users to assign a role.
|
|
19
|
+
#
|
|
20
|
+
class UserRole < BaseEntity
|
|
21
|
+
def self.entity_path
|
|
22
|
+
"users/roles"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_s
|
|
26
|
+
name.to_s
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOCO
|
|
4
|
+
# Represents a MOCO purchase VAT code (Steuerschlüssel Einkauf)
|
|
5
|
+
# Read-only VAT rates for purchases/receipts
|
|
6
|
+
#
|
|
7
|
+
# == Read-only attributes:
|
|
8
|
+
# id, code, tax, reverse_charge, intra_eu, active
|
|
9
|
+
#
|
|
10
|
+
# == Filtering:
|
|
11
|
+
# moco.vat_code_purchases.where(active: true)
|
|
12
|
+
# moco.vat_code_purchases.where(reverse_charge: true)
|
|
13
|
+
# moco.vat_code_purchases.where(intra_eu: true)
|
|
14
|
+
# moco.vat_code_purchases.where(ids: "123,456")
|
|
15
|
+
#
|
|
16
|
+
# == Note:
|
|
17
|
+
# VAT codes are configured in MOCO's admin interface.
|
|
18
|
+
# Use vat_code_id when creating purchases/receipts.
|
|
19
|
+
#
|
|
20
|
+
class VatCodePurchase < BaseEntity
|
|
21
|
+
def self.entity_path
|
|
22
|
+
"vat_code_purchases"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_s
|
|
26
|
+
"#{code} - #{name}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOCO
|
|
4
|
+
# Represents a MOCO sales VAT code (Steuerschlüssel Verkauf)
|
|
5
|
+
# Read-only VAT rates for invoices/offers
|
|
6
|
+
#
|
|
7
|
+
# == Read-only attributes:
|
|
8
|
+
# id, code, tax, reverse_charge, intra_eu, active,
|
|
9
|
+
# print_gross_total, notice_tax_exemption, credit_account
|
|
10
|
+
#
|
|
11
|
+
# == Filtering:
|
|
12
|
+
# moco.vat_code_sales.where(active: true)
|
|
13
|
+
# moco.vat_code_sales.where(reverse_charge: true)
|
|
14
|
+
# moco.vat_code_sales.where(intra_eu: true) # EU intra-community
|
|
15
|
+
#
|
|
16
|
+
# == Note:
|
|
17
|
+
# VAT codes are configured in MOCO's admin interface.
|
|
18
|
+
# Use vat_code_id when creating invoices/offers.
|
|
19
|
+
#
|
|
20
|
+
class VatCodeSale < BaseEntity
|
|
21
|
+
def self.entity_path
|
|
22
|
+
"vat_code_sales"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_s
|
|
26
|
+
"#{code} - #{name}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -1,8 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module MOCO
|
|
4
|
-
# Represents a MOCO webhook
|
|
5
|
-
#
|
|
4
|
+
# Represents a MOCO webhook for event notifications
|
|
5
|
+
#
|
|
6
|
+
# == Required attributes for create:
|
|
7
|
+
# target - String, entity type to watch:
|
|
8
|
+
# "Activity", "Company", "Contact", "Project",
|
|
9
|
+
# "Invoice", "Offer", "Deal", "Expense"
|
|
10
|
+
# event - String, event type: "create", "update", "delete"
|
|
11
|
+
# hook - String, URL to receive webhook payloads (e.g., "https://example.org/callback")
|
|
12
|
+
#
|
|
13
|
+
# == Read-only attributes:
|
|
14
|
+
# id, disabled, disabled_at, created_at, updated_at
|
|
15
|
+
#
|
|
16
|
+
# == Instance methods:
|
|
17
|
+
# enable - Enable the webhook
|
|
18
|
+
# disable - Disable the webhook
|
|
19
|
+
#
|
|
20
|
+
# == Example:
|
|
21
|
+
# # Create webhook for new activities
|
|
22
|
+
# moco.web_hooks.create(
|
|
23
|
+
# target: "Activity",
|
|
24
|
+
# event: "create",
|
|
25
|
+
# hook: "https://example.org/moco-activity-webhook"
|
|
26
|
+
# )
|
|
27
|
+
#
|
|
28
|
+
# # Disable a webhook
|
|
29
|
+
# webhook = moco.web_hooks.find(123)
|
|
30
|
+
# webhook.disable
|
|
31
|
+
#
|
|
32
|
+
# == Note:
|
|
33
|
+
# Only the `hook` URL can be updated after creation.
|
|
34
|
+
# To change target/event, delete and recreate the webhook.
|
|
35
|
+
#
|
|
6
36
|
class WebHook < BaseEntity
|
|
7
37
|
# Override entity_path to match API path
|
|
8
38
|
def self.entity_path
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOCO
|
|
4
|
+
# Represents a MOCO user work time adjustment record
|
|
5
|
+
# (Korrekturen Zeiterfassung) for overtime/undertime corrections
|
|
6
|
+
#
|
|
7
|
+
# == Required attributes for create:
|
|
8
|
+
# user_id - Integer, user to adjust
|
|
9
|
+
# date - String, "YYYY-MM-DD" effective date
|
|
10
|
+
# description - String, reason for adjustment
|
|
11
|
+
# hours - Float, hours to add (positive) or subtract (negative)
|
|
12
|
+
#
|
|
13
|
+
# == Read-only attributes:
|
|
14
|
+
# id, user (Hash), creator (Hash), created_at, updated_at
|
|
15
|
+
#
|
|
16
|
+
# == Example:
|
|
17
|
+
# # Add overtime from previous year
|
|
18
|
+
# moco.work_time_adjustments.create(
|
|
19
|
+
# user_id: 123,
|
|
20
|
+
# date: "2024-01-01",
|
|
21
|
+
# description: "Overtime carryover from 2023",
|
|
22
|
+
# hours: 42.0
|
|
23
|
+
# )
|
|
24
|
+
#
|
|
25
|
+
# # Correct time balance
|
|
26
|
+
# moco.work_time_adjustments.create(
|
|
27
|
+
# user_id: 123,
|
|
28
|
+
# date: "2024-06-15",
|
|
29
|
+
# description: "Correction for unpaid leave",
|
|
30
|
+
# hours: -16.0
|
|
31
|
+
# )
|
|
32
|
+
#
|
|
33
|
+
# == Filtering:
|
|
34
|
+
# moco.work_time_adjustments.where(user_id: 123)
|
|
35
|
+
# moco.work_time_adjustments.where(from: "2024-01-01", to: "2024-12-31")
|
|
36
|
+
#
|
|
37
|
+
class WorkTimeAdjustment < BaseEntity
|
|
38
|
+
def self.entity_path
|
|
39
|
+
"users/work_time_adjustments"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Associations
|
|
43
|
+
def user
|
|
44
|
+
association(:user)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_s
|
|
48
|
+
"WorkTimeAdjustment ##{id} (#{date})"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/moco/sync.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "set"
|
|
3
4
|
require "fuzzy_match"
|
|
4
5
|
require_relative "client"
|
|
5
6
|
|
|
@@ -17,12 +18,15 @@ module MOCO
|
|
|
17
18
|
@filters = args.fetch(:filters, {})
|
|
18
19
|
@dry_run = args.fetch(:dry_run, false)
|
|
19
20
|
@debug = args.fetch(:debug, false)
|
|
21
|
+
@default_task_name = args.fetch(:default_task_name, nil)
|
|
20
22
|
|
|
21
23
|
@project_mapping = {}
|
|
22
24
|
@task_mapping = {}
|
|
25
|
+
@default_task_cache = {} # Cache default tasks per project
|
|
23
26
|
|
|
24
27
|
fetch_assigned_projects
|
|
25
28
|
build_initial_mappings
|
|
29
|
+
create_missing_tasks_for_activities
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
# rubocop:todo Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
@@ -420,6 +424,110 @@ module MOCO
|
|
|
420
424
|
end
|
|
421
425
|
end
|
|
422
426
|
|
|
427
|
+
def create_missing_tasks_for_activities
|
|
428
|
+
# Fetch source activities to see which tasks are actually used
|
|
429
|
+
source_activity_filters = @filters.fetch(:source, {})
|
|
430
|
+
source_activities = @source.activities.where(source_activity_filters).all
|
|
431
|
+
|
|
432
|
+
# Collect unique task IDs that are used in activities and need syncing
|
|
433
|
+
tasks_needed = Set.new
|
|
434
|
+
source_activities.each do |activity|
|
|
435
|
+
# Only consider activities for mapped projects
|
|
436
|
+
next unless @project_mapping[activity.project&.id]
|
|
437
|
+
# Check if task is already mapped
|
|
438
|
+
next if activity.task.nil?
|
|
439
|
+
next if @task_mapping[activity.task.id]
|
|
440
|
+
|
|
441
|
+
tasks_needed.add(activity.task.id)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
return if tasks_needed.empty?
|
|
445
|
+
|
|
446
|
+
debug_log "Found #{tasks_needed.size} unmapped tasks used in activities"
|
|
447
|
+
|
|
448
|
+
# Track tasks that couldn't be created due to permission errors
|
|
449
|
+
@failed_task_creations ||= []
|
|
450
|
+
@mapped_to_default ||= []
|
|
451
|
+
|
|
452
|
+
# Create missing tasks in target projects
|
|
453
|
+
tasks_needed.each do |task_id|
|
|
454
|
+
# Find the source task from source activities
|
|
455
|
+
source_activity = source_activities.find { |a| a.task&.id == task_id }
|
|
456
|
+
next unless source_activity
|
|
457
|
+
|
|
458
|
+
source_task = source_activity.task
|
|
459
|
+
source_project_id = source_activity.project.id
|
|
460
|
+
target_project = @project_mapping[source_project_id]
|
|
461
|
+
|
|
462
|
+
# If default task name is provided, try to map to it instead of creating
|
|
463
|
+
if @default_task_name
|
|
464
|
+
default_task = find_default_task(target_project)
|
|
465
|
+
if default_task
|
|
466
|
+
@task_mapping[source_task.id] = default_task
|
|
467
|
+
debug_log " Mapped task '#{source_task.name}' -> default task '#{default_task.name}' (#{default_task.id})"
|
|
468
|
+
@mapped_to_default << {
|
|
469
|
+
task_name: source_task.name,
|
|
470
|
+
project_name: target_project.name,
|
|
471
|
+
default_task_name: default_task.name
|
|
472
|
+
}
|
|
473
|
+
next
|
|
474
|
+
else
|
|
475
|
+
warn " WARNING: Default task '#{@default_task_name}' not found in target project '#{target_project.name}'"
|
|
476
|
+
warn " Will attempt to create task '#{source_task.name}' instead"
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
debug_log " Creating missing task '#{source_task.name}' in target project #{target_project.id} (#{target_project.name})"
|
|
481
|
+
|
|
482
|
+
unless @dry_run
|
|
483
|
+
begin
|
|
484
|
+
# Create the task in the target project
|
|
485
|
+
# Tasks used in activities must be active
|
|
486
|
+
# Use NestedCollectionProxy to create the task
|
|
487
|
+
task_proxy = MOCO::NestedCollectionProxy.new(@target, target_project, :tasks, "Task")
|
|
488
|
+
new_task = task_proxy.create(
|
|
489
|
+
name: source_task.name,
|
|
490
|
+
billable: source_task.billable,
|
|
491
|
+
active: true
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
# Add to mapping
|
|
495
|
+
@task_mapping[source_task.id] = new_task
|
|
496
|
+
debug_log " Created task #{new_task.id} - #{new_task.name}"
|
|
497
|
+
rescue StandardError => e
|
|
498
|
+
# Check if this is a permission error
|
|
499
|
+
if e.message =~ /403|Forbidden|401|Unauthorized|not authorized|permission/i
|
|
500
|
+
warn " WARNING: Cannot create task '#{source_task.name}' in target project - insufficient permissions"
|
|
501
|
+
warn " Activities using this task will be skipped during sync"
|
|
502
|
+
@failed_task_creations << {
|
|
503
|
+
task_name: source_task.name,
|
|
504
|
+
project_name: target_project.name,
|
|
505
|
+
project_id: target_project.id
|
|
506
|
+
}
|
|
507
|
+
else
|
|
508
|
+
# Re-raise other errors
|
|
509
|
+
raise
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
else
|
|
513
|
+
debug_log " (Dry run - would create task '#{source_task.name}')"
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def find_default_task(target_project)
|
|
519
|
+
# Return cached result if available
|
|
520
|
+
return @default_task_cache[target_project.id] if @default_task_cache.key?(target_project.id)
|
|
521
|
+
|
|
522
|
+
# Search for the default task in the target project
|
|
523
|
+
default_task = target_project.tasks.find { |task| task.name == @default_task_name }
|
|
524
|
+
|
|
525
|
+
# Cache the result (even if nil)
|
|
526
|
+
@default_task_cache[target_project.id] = default_task
|
|
527
|
+
|
|
528
|
+
default_task
|
|
529
|
+
end
|
|
530
|
+
|
|
423
531
|
def match_project(target_project)
|
|
424
532
|
# Create array of search objects manually since we can't call map on EntityCollection
|
|
425
533
|
searchable_projects = []
|
|
@@ -436,19 +544,17 @@ module MOCO
|
|
|
436
544
|
end
|
|
437
545
|
|
|
438
546
|
def match_task(target_task, source_project)
|
|
439
|
-
# Get tasks from the source project
|
|
547
|
+
# Get tasks from the source project (embedded in projects.assigned response)
|
|
440
548
|
tasks = source_project.tasks
|
|
441
549
|
|
|
442
|
-
#
|
|
550
|
+
# Only proceed if we have tasks to match against
|
|
551
|
+
return nil if tasks.empty?
|
|
443
552
|
|
|
444
|
-
#
|
|
553
|
+
# Create array of search objects for fuzzy matching
|
|
445
554
|
searchable_tasks = tasks.map do |task|
|
|
446
555
|
{ original: task, name: task.name }
|
|
447
556
|
end
|
|
448
557
|
|
|
449
|
-
# Only proceed if we have tasks to match against
|
|
450
|
-
return nil if searchable_tasks.empty?
|
|
451
|
-
|
|
452
558
|
matcher = FuzzyMatch.new(searchable_tasks, read: :name)
|
|
453
559
|
match = matcher.find(target_task.name, threshold: @task_match_threshold)
|
|
454
560
|
match[:original] if match
|
data/lib/moco/version.rb
CHANGED
data/lib/moco-ruby.rb
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This file exists so that `gem 'moco-ruby'` auto-requires correctly.
|
|
4
|
+
# Bundler converts gem names with hyphens to require paths with slashes,
|
|
5
|
+
# so 'moco-ruby' looks for 'moco/ruby'. This shim redirects to the real entry point.
|
|
6
|
+
require_relative "moco"
|