xero-kiwi 0.1.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 +7 -0
- data/.env.example +2 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +89 -0
- data/Rakefile +89 -0
- data/docs/accounting/address.md +54 -0
- data/docs/accounting/branding-theme.md +92 -0
- data/docs/accounting/contact-group.md +91 -0
- data/docs/accounting/contact.md +166 -0
- data/docs/accounting/credit-note.md +97 -0
- data/docs/accounting/external-link.md +33 -0
- data/docs/accounting/invoice.md +134 -0
- data/docs/accounting/organisation.md +119 -0
- data/docs/accounting/overpayment.md +94 -0
- data/docs/accounting/payment-terms.md +58 -0
- data/docs/accounting/payment.md +99 -0
- data/docs/accounting/phone.md +45 -0
- data/docs/accounting/prepayment.md +111 -0
- data/docs/accounting/user.md +109 -0
- data/docs/client.md +174 -0
- data/docs/connections.md +166 -0
- data/docs/errors.md +271 -0
- data/docs/getting-started.md +138 -0
- data/docs/oauth.md +508 -0
- data/docs/retries-and-rate-limits.md +224 -0
- data/docs/tokens.md +339 -0
- data/lib/xero_kiwi/accounting/address.rb +58 -0
- data/lib/xero_kiwi/accounting/allocation.rb +66 -0
- data/lib/xero_kiwi/accounting/branding_theme.rb +76 -0
- data/lib/xero_kiwi/accounting/contact.rb +153 -0
- data/lib/xero_kiwi/accounting/contact_group.rb +57 -0
- data/lib/xero_kiwi/accounting/contact_person.rb +45 -0
- data/lib/xero_kiwi/accounting/credit_note.rb +115 -0
- data/lib/xero_kiwi/accounting/external_link.rb +38 -0
- data/lib/xero_kiwi/accounting/invoice.rb +142 -0
- data/lib/xero_kiwi/accounting/line_item.rb +64 -0
- data/lib/xero_kiwi/accounting/organisation.rb +138 -0
- data/lib/xero_kiwi/accounting/overpayment.rb +107 -0
- data/lib/xero_kiwi/accounting/payment.rb +105 -0
- data/lib/xero_kiwi/accounting/payment_terms.rb +77 -0
- data/lib/xero_kiwi/accounting/phone.rb +46 -0
- data/lib/xero_kiwi/accounting/prepayment.rb +109 -0
- data/lib/xero_kiwi/accounting/tracking_category.rb +42 -0
- data/lib/xero_kiwi/accounting/user.rb +80 -0
- data/lib/xero_kiwi/client.rb +576 -0
- data/lib/xero_kiwi/connection.rb +78 -0
- data/lib/xero_kiwi/errors.rb +34 -0
- data/lib/xero_kiwi/identity.rb +40 -0
- data/lib/xero_kiwi/oauth/id_token.rb +102 -0
- data/lib/xero_kiwi/oauth/pkce.rb +51 -0
- data/lib/xero_kiwi/oauth.rb +232 -0
- data/lib/xero_kiwi/token.rb +99 -0
- data/lib/xero_kiwi/token_refresher.rb +53 -0
- data/lib/xero_kiwi/version.rb +5 -0
- data/lib/xero_kiwi.rb +33 -0
- data/llms-full.txt +3351 -0
- data/llms.txt +56 -0
- data/sig/xero_kiwi.rbs +4 -0
- metadata +164 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XeroKiwi
|
|
4
|
+
module Accounting
|
|
5
|
+
# Represents a line item nested within Xero documents (invoices, credit
|
|
6
|
+
# notes, prepayments, overpayments).
|
|
7
|
+
#
|
|
8
|
+
# See: https://developer.xero.com/documentation/api/accounting/invoices
|
|
9
|
+
class LineItem
|
|
10
|
+
ATTRIBUTES = {
|
|
11
|
+
line_item_id: "LineItemID",
|
|
12
|
+
description: "Description",
|
|
13
|
+
quantity: "Quantity",
|
|
14
|
+
unit_amount: "UnitAmount",
|
|
15
|
+
item_code: "ItemCode",
|
|
16
|
+
account_code: "AccountCode",
|
|
17
|
+
account_id: "AccountId",
|
|
18
|
+
tax_type: "TaxType",
|
|
19
|
+
tax_amount: "TaxAmount",
|
|
20
|
+
line_amount: "LineAmount",
|
|
21
|
+
discount_rate: "DiscountRate",
|
|
22
|
+
discount_amount: "DiscountAmount",
|
|
23
|
+
tracking: "Tracking",
|
|
24
|
+
item: "Item"
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
attr_reader(*ATTRIBUTES.keys)
|
|
28
|
+
|
|
29
|
+
def initialize(attrs) # rubocop:disable Metrics/AbcSize
|
|
30
|
+
attrs = attrs.transform_keys(&:to_s)
|
|
31
|
+
@line_item_id = attrs["LineItemID"]
|
|
32
|
+
@description = attrs["Description"]
|
|
33
|
+
@quantity = attrs["Quantity"]
|
|
34
|
+
@unit_amount = attrs["UnitAmount"]
|
|
35
|
+
@item_code = attrs["ItemCode"]
|
|
36
|
+
@account_code = attrs["AccountCode"]
|
|
37
|
+
@account_id = attrs["AccountId"]
|
|
38
|
+
@tax_type = attrs["TaxType"]
|
|
39
|
+
@tax_amount = attrs["TaxAmount"]
|
|
40
|
+
@line_amount = attrs["LineAmount"]
|
|
41
|
+
@discount_rate = attrs["DiscountRate"]
|
|
42
|
+
@discount_amount = attrs["DiscountAmount"]
|
|
43
|
+
@tracking = (attrs["Tracking"] || []).map { |t| TrackingCategory.new(t) }
|
|
44
|
+
@item = attrs["Item"]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_h
|
|
48
|
+
ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def ==(other)
|
|
52
|
+
other.is_a?(LineItem) && to_h == other.to_h
|
|
53
|
+
end
|
|
54
|
+
alias eql? ==
|
|
55
|
+
|
|
56
|
+
def hash = to_h.hash
|
|
57
|
+
|
|
58
|
+
def inspect
|
|
59
|
+
"#<#{self.class} description=#{description.inspect} " \
|
|
60
|
+
"line_amount=#{line_amount.inspect}>"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module XeroKiwi
|
|
6
|
+
module Accounting
|
|
7
|
+
# Represents a Xero Organisation (tenant) returned by the Accounting API.
|
|
8
|
+
#
|
|
9
|
+
# See: https://developer.xero.com/documentation/api/accounting/organisation
|
|
10
|
+
class Organisation
|
|
11
|
+
ATTRIBUTES = {
|
|
12
|
+
organisation_id: "OrganisationID",
|
|
13
|
+
api_key: "APIKey",
|
|
14
|
+
name: "Name",
|
|
15
|
+
legal_name: "LegalName",
|
|
16
|
+
pays_tax: "PaysTax",
|
|
17
|
+
version: "Version",
|
|
18
|
+
organisation_type: "OrganisationType",
|
|
19
|
+
base_currency: "BaseCurrency",
|
|
20
|
+
country_code: "CountryCode",
|
|
21
|
+
is_demo_company: "IsDemoCompany",
|
|
22
|
+
organisation_status: "OrganisationStatus",
|
|
23
|
+
registration_number: "RegistrationNumber",
|
|
24
|
+
employer_identification_number: "EmployerIdentificationNumber",
|
|
25
|
+
tax_number: "TaxNumber",
|
|
26
|
+
financial_year_end_day: "FinancialYearEndDay",
|
|
27
|
+
financial_year_end_month: "FinancialYearEndMonth",
|
|
28
|
+
sales_tax_basis: "SalesTaxBasis",
|
|
29
|
+
sales_tax_period: "SalesTaxPeriod",
|
|
30
|
+
default_sales_tax: "DefaultSalesTax",
|
|
31
|
+
default_purchases_tax: "DefaultPurchasesTax",
|
|
32
|
+
period_lock_date: "PeriodLockDate",
|
|
33
|
+
end_of_year_lock_date: "EndOfYearLockDate",
|
|
34
|
+
created_date_utc: "CreatedDateUTC",
|
|
35
|
+
timezone: "Timezone",
|
|
36
|
+
organisation_entity_type: "OrganisationEntityType",
|
|
37
|
+
short_code: "ShortCode",
|
|
38
|
+
organisation_class: "Class",
|
|
39
|
+
edition: "Edition",
|
|
40
|
+
line_of_business: "LineOfBusiness",
|
|
41
|
+
addresses: "Addresses",
|
|
42
|
+
phones: "Phones",
|
|
43
|
+
external_links: "ExternalLinks",
|
|
44
|
+
payment_terms: "PaymentTerms"
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
attr_reader(*ATTRIBUTES.keys)
|
|
48
|
+
|
|
49
|
+
def self.from_response(payload)
|
|
50
|
+
return nil if payload.nil?
|
|
51
|
+
|
|
52
|
+
items = payload["Organisations"]
|
|
53
|
+
return nil if items.nil? || items.empty?
|
|
54
|
+
|
|
55
|
+
new(items.first)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def initialize(attrs) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
|
|
59
|
+
attrs = attrs.transform_keys(&:to_s)
|
|
60
|
+
@organisation_id = attrs["OrganisationID"]
|
|
61
|
+
@api_key = attrs["APIKey"]
|
|
62
|
+
@name = attrs["Name"]
|
|
63
|
+
@legal_name = attrs["LegalName"]
|
|
64
|
+
@pays_tax = attrs["PaysTax"]
|
|
65
|
+
@version = attrs["Version"]
|
|
66
|
+
@organisation_type = attrs["OrganisationType"]
|
|
67
|
+
@base_currency = attrs["BaseCurrency"]
|
|
68
|
+
@country_code = attrs["CountryCode"]
|
|
69
|
+
@is_demo_company = attrs["IsDemoCompany"]
|
|
70
|
+
@organisation_status = attrs["OrganisationStatus"]
|
|
71
|
+
@registration_number = attrs["RegistrationNumber"]
|
|
72
|
+
@employer_identification_number = attrs["EmployerIdentificationNumber"]
|
|
73
|
+
@tax_number = attrs["TaxNumber"]
|
|
74
|
+
@financial_year_end_day = attrs["FinancialYearEndDay"]
|
|
75
|
+
@financial_year_end_month = attrs["FinancialYearEndMonth"]
|
|
76
|
+
@sales_tax_basis = attrs["SalesTaxBasis"]
|
|
77
|
+
@sales_tax_period = attrs["SalesTaxPeriod"]
|
|
78
|
+
@default_sales_tax = attrs["DefaultSalesTax"]
|
|
79
|
+
@default_purchases_tax = attrs["DefaultPurchasesTax"]
|
|
80
|
+
@period_lock_date = parse_time(attrs["PeriodLockDate"])
|
|
81
|
+
@end_of_year_lock_date = parse_time(attrs["EndOfYearLockDate"])
|
|
82
|
+
@created_date_utc = parse_time(attrs["CreatedDateUTC"])
|
|
83
|
+
@timezone = attrs["Timezone"]
|
|
84
|
+
@organisation_entity_type = attrs["OrganisationEntityType"]
|
|
85
|
+
@short_code = attrs["ShortCode"]
|
|
86
|
+
@organisation_class = attrs["Class"]
|
|
87
|
+
@edition = attrs["Edition"]
|
|
88
|
+
@line_of_business = attrs["LineOfBusiness"]
|
|
89
|
+
@addresses = (attrs["Addresses"] || []).map { |a| Address.new(a) }
|
|
90
|
+
@phones = (attrs["Phones"] || []).map { |p| Phone.new(p) }
|
|
91
|
+
@external_links = (attrs["ExternalLinks"] || []).map { |l| ExternalLink.new(l) }
|
|
92
|
+
@payment_terms = PaymentTerms.from_hash(attrs["PaymentTerms"])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def demo_company? = is_demo_company == true
|
|
96
|
+
|
|
97
|
+
def to_h
|
|
98
|
+
ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def ==(other)
|
|
102
|
+
other.is_a?(Organisation) && other.organisation_id == organisation_id
|
|
103
|
+
end
|
|
104
|
+
alias eql? ==
|
|
105
|
+
|
|
106
|
+
def hash = [self.class, organisation_id].hash
|
|
107
|
+
|
|
108
|
+
def inspect
|
|
109
|
+
"#<#{self.class} organisation_id=#{organisation_id.inspect} " \
|
|
110
|
+
"name=#{name.inspect} organisation_type=#{organisation_type.inspect}>"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
# Xero uses two timestamp formats depending on the endpoint:
|
|
116
|
+
#
|
|
117
|
+
# - ISO 8601: "2019-07-09T23:40:30.1833130" (connections API)
|
|
118
|
+
# - .NET JSON: "/Date(1574275974000)/" (accounting API)
|
|
119
|
+
#
|
|
120
|
+
# Handle both, always returning UTC.
|
|
121
|
+
def parse_time(value)
|
|
122
|
+
return nil if value.nil?
|
|
123
|
+
|
|
124
|
+
str = value.to_s.strip
|
|
125
|
+
return nil if str.empty?
|
|
126
|
+
|
|
127
|
+
if (match = str.match(%r{\A/Date\((\d+)([+-]\d{4})?\)/\z}))
|
|
128
|
+
Time.at(match[1].to_i / 1000.0).utc
|
|
129
|
+
else
|
|
130
|
+
str = "#{str}Z" unless str.match?(/[Zz]\z|[+-]\d{2}:?\d{2}\z/)
|
|
131
|
+
Time.iso8601(str)
|
|
132
|
+
end
|
|
133
|
+
rescue ArgumentError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module XeroKiwi
|
|
6
|
+
module Accounting
|
|
7
|
+
# Represents a Xero Overpayment returned by the Accounting API.
|
|
8
|
+
#
|
|
9
|
+
# See: https://developer.xero.com/documentation/api/accounting/overpayments
|
|
10
|
+
class Overpayment
|
|
11
|
+
ATTRIBUTES = {
|
|
12
|
+
overpayment_id: "OverpaymentID",
|
|
13
|
+
type: "Type",
|
|
14
|
+
contact: "Contact",
|
|
15
|
+
date: "Date",
|
|
16
|
+
status: "Status",
|
|
17
|
+
line_amount_types: "LineAmountTypes",
|
|
18
|
+
line_items: "LineItems",
|
|
19
|
+
sub_total: "SubTotal",
|
|
20
|
+
total_tax: "TotalTax",
|
|
21
|
+
total: "Total",
|
|
22
|
+
updated_date_utc: "UpdatedDateUTC",
|
|
23
|
+
currency_code: "CurrencyCode",
|
|
24
|
+
currency_rate: "CurrencyRate",
|
|
25
|
+
remaining_credit: "RemainingCredit",
|
|
26
|
+
allocations: "Allocations",
|
|
27
|
+
payments: "Payments",
|
|
28
|
+
has_attachments: "HasAttachments",
|
|
29
|
+
reference: "Reference"
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
attr_reader(*ATTRIBUTES.keys)
|
|
33
|
+
|
|
34
|
+
def self.from_response(payload)
|
|
35
|
+
return [] if payload.nil?
|
|
36
|
+
|
|
37
|
+
items = payload["Overpayments"]
|
|
38
|
+
return [] if items.nil?
|
|
39
|
+
|
|
40
|
+
items.map { |attrs| new(attrs) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(attrs, reference: false) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
44
|
+
attrs = attrs.transform_keys(&:to_s)
|
|
45
|
+
@is_reference = reference
|
|
46
|
+
@overpayment_id = attrs["OverpaymentID"]
|
|
47
|
+
@type = attrs["Type"]
|
|
48
|
+
@contact = attrs["Contact"] ? Contact.new(attrs["Contact"], reference: true) : nil
|
|
49
|
+
@date = parse_time(attrs["Date"])
|
|
50
|
+
@status = attrs["Status"]
|
|
51
|
+
@line_amount_types = attrs["LineAmountTypes"]
|
|
52
|
+
@line_items = (attrs["LineItems"] || []).map { |li| LineItem.new(li) }
|
|
53
|
+
@sub_total = attrs["SubTotal"]
|
|
54
|
+
@total_tax = attrs["TotalTax"]
|
|
55
|
+
@total = attrs["Total"]
|
|
56
|
+
@updated_date_utc = parse_time(attrs["UpdatedDateUTC"])
|
|
57
|
+
@currency_code = attrs["CurrencyCode"]
|
|
58
|
+
@currency_rate = attrs["CurrencyRate"]
|
|
59
|
+
@remaining_credit = attrs["RemainingCredit"]
|
|
60
|
+
@allocations = (attrs["Allocations"] || []).map { |a| Allocation.new(a) }
|
|
61
|
+
@payments = (attrs["Payments"] || []).map { |p| Payment.new(p, reference: true) }
|
|
62
|
+
@has_attachments = attrs["HasAttachments"]
|
|
63
|
+
@reference = attrs["Reference"]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def receive? = type == "RECEIVE-OVERPAYMENT"
|
|
67
|
+
|
|
68
|
+
def spend? = type == "SPEND-OVERPAYMENT"
|
|
69
|
+
|
|
70
|
+
def reference? = @is_reference
|
|
71
|
+
|
|
72
|
+
def to_h
|
|
73
|
+
ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def ==(other)
|
|
77
|
+
other.is_a?(Overpayment) && other.overpayment_id == overpayment_id
|
|
78
|
+
end
|
|
79
|
+
alias eql? ==
|
|
80
|
+
|
|
81
|
+
def hash = [self.class, overpayment_id].hash
|
|
82
|
+
|
|
83
|
+
def inspect
|
|
84
|
+
"#<#{self.class} overpayment_id=#{overpayment_id.inspect} " \
|
|
85
|
+
"type=#{type.inspect} status=#{status.inspect} total=#{total.inspect}>"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def parse_time(value)
|
|
91
|
+
return nil if value.nil?
|
|
92
|
+
|
|
93
|
+
str = value.to_s.strip
|
|
94
|
+
return nil if str.empty?
|
|
95
|
+
|
|
96
|
+
if (match = str.match(%r{\A/Date\((\d+)([+-]\d{4})?\)/\z}))
|
|
97
|
+
Time.at(match[1].to_i / 1000.0).utc
|
|
98
|
+
else
|
|
99
|
+
str = "#{str}Z" unless str.match?(/[Zz]\z|[+-]\d{2}:?\d{2}\z/)
|
|
100
|
+
Time.iso8601(str)
|
|
101
|
+
end
|
|
102
|
+
rescue ArgumentError
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module XeroKiwi
|
|
6
|
+
module Accounting
|
|
7
|
+
# Represents a Xero Payment returned by the Accounting API.
|
|
8
|
+
#
|
|
9
|
+
# See: https://developer.xero.com/documentation/api/accounting/payments
|
|
10
|
+
class Payment
|
|
11
|
+
ATTRIBUTES = {
|
|
12
|
+
payment_id: "PaymentID",
|
|
13
|
+
date: "Date",
|
|
14
|
+
currency_rate: "CurrencyRate",
|
|
15
|
+
amount: "Amount",
|
|
16
|
+
bank_amount: "BankAmount",
|
|
17
|
+
reference: "Reference",
|
|
18
|
+
is_reconciled: "IsReconciled",
|
|
19
|
+
status: "Status",
|
|
20
|
+
payment_type: "PaymentType",
|
|
21
|
+
updated_date_utc: "UpdatedDateUTC",
|
|
22
|
+
batch_payment_id: "BatchPaymentID",
|
|
23
|
+
batch_payment: "BatchPayment",
|
|
24
|
+
account: "Account",
|
|
25
|
+
invoice: "Invoice",
|
|
26
|
+
credit_note: "CreditNote",
|
|
27
|
+
prepayment: "Prepayment",
|
|
28
|
+
overpayment: "Overpayment",
|
|
29
|
+
has_account: "HasAccount"
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
attr_reader(*ATTRIBUTES.keys)
|
|
33
|
+
|
|
34
|
+
def self.from_response(payload)
|
|
35
|
+
return [] if payload.nil?
|
|
36
|
+
|
|
37
|
+
items = payload["Payments"]
|
|
38
|
+
return [] if items.nil?
|
|
39
|
+
|
|
40
|
+
items.map { |attrs| new(attrs) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(attrs, reference: false) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
44
|
+
attrs = attrs.transform_keys(&:to_s)
|
|
45
|
+
@is_reference = reference
|
|
46
|
+
@payment_id = attrs["PaymentID"]
|
|
47
|
+
@date = parse_time(attrs["Date"])
|
|
48
|
+
@currency_rate = attrs["CurrencyRate"]
|
|
49
|
+
@amount = attrs["Amount"]
|
|
50
|
+
@bank_amount = attrs["BankAmount"]
|
|
51
|
+
@reference = attrs["Reference"]
|
|
52
|
+
@is_reconciled = attrs["IsReconciled"]
|
|
53
|
+
@status = attrs["Status"]
|
|
54
|
+
@payment_type = attrs["PaymentType"]
|
|
55
|
+
@updated_date_utc = parse_time(attrs["UpdatedDateUTC"])
|
|
56
|
+
@batch_payment_id = attrs["BatchPaymentID"]
|
|
57
|
+
@batch_payment = attrs["BatchPayment"]
|
|
58
|
+
@account = attrs["Account"]
|
|
59
|
+
@invoice = attrs["Invoice"] ? Invoice.new(attrs["Invoice"], reference: true) : nil
|
|
60
|
+
@credit_note = attrs["CreditNote"] ? CreditNote.new(attrs["CreditNote"], reference: true) : nil
|
|
61
|
+
@prepayment = attrs["Prepayment"] ? Prepayment.new(attrs["Prepayment"], reference: true) : nil
|
|
62
|
+
@overpayment = attrs["Overpayment"] ? Overpayment.new(attrs["Overpayment"], reference: true) : nil
|
|
63
|
+
@has_account = attrs["HasAccount"]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def reference? = @is_reference
|
|
67
|
+
|
|
68
|
+
def reconciled? = is_reconciled == true
|
|
69
|
+
|
|
70
|
+
def to_h
|
|
71
|
+
ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def ==(other)
|
|
75
|
+
other.is_a?(Payment) && other.payment_id == payment_id
|
|
76
|
+
end
|
|
77
|
+
alias eql? ==
|
|
78
|
+
|
|
79
|
+
def hash = [self.class, payment_id].hash
|
|
80
|
+
|
|
81
|
+
def inspect
|
|
82
|
+
"#<#{self.class} payment_id=#{payment_id.inspect} " \
|
|
83
|
+
"payment_type=#{payment_type.inspect} status=#{status.inspect} amount=#{amount.inspect}>"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def parse_time(value)
|
|
89
|
+
return nil if value.nil?
|
|
90
|
+
|
|
91
|
+
str = value.to_s.strip
|
|
92
|
+
return nil if str.empty?
|
|
93
|
+
|
|
94
|
+
if (match = str.match(%r{\A/Date\((\d+)([+-]\d{4})?\)/\z}))
|
|
95
|
+
Time.at(match[1].to_i / 1000.0).utc
|
|
96
|
+
else
|
|
97
|
+
str = "#{str}Z" unless str.match?(/[Zz]\z|[+-]\d{2}:?\d{2}\z/)
|
|
98
|
+
Time.iso8601(str)
|
|
99
|
+
end
|
|
100
|
+
rescue ArgumentError
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XeroKiwi
|
|
4
|
+
module Accounting
|
|
5
|
+
# Xero payment terms for an Organisation or Contact, containing separate
|
|
6
|
+
# terms for bills (payable) and sales (receivable).
|
|
7
|
+
#
|
|
8
|
+
# See: https://developer.xero.com/documentation/api/accounting/types#paymentterms
|
|
9
|
+
class PaymentTerms
|
|
10
|
+
attr_reader :bills, :sales
|
|
11
|
+
|
|
12
|
+
def self.from_hash(hash)
|
|
13
|
+
return nil if hash.nil?
|
|
14
|
+
|
|
15
|
+
new(hash)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(attrs)
|
|
19
|
+
attrs = attrs.transform_keys(&:to_s)
|
|
20
|
+
@bills = PaymentTerm.from_hash(attrs["Bills"])
|
|
21
|
+
@sales = PaymentTerm.from_hash(attrs["Sales"])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{ bills: bills&.to_h, sales: sales&.to_h }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ==(other)
|
|
29
|
+
other.is_a?(PaymentTerms) && bills == other.bills && sales == other.sales
|
|
30
|
+
end
|
|
31
|
+
alias eql? ==
|
|
32
|
+
|
|
33
|
+
def hash = [self.class, bills, sales].hash
|
|
34
|
+
|
|
35
|
+
def inspect
|
|
36
|
+
"#<#{self.class} bills=#{bills.inspect} sales=#{sales.inspect}>"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# A single payment term (either bills or sales side).
|
|
41
|
+
class PaymentTerm
|
|
42
|
+
ATTRIBUTES = {
|
|
43
|
+
day: "Day",
|
|
44
|
+
type: "Type"
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
attr_reader(*ATTRIBUTES.keys)
|
|
48
|
+
|
|
49
|
+
def self.from_hash(hash)
|
|
50
|
+
return nil if hash.nil? || hash.empty?
|
|
51
|
+
|
|
52
|
+
new(hash)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initialize(attrs)
|
|
56
|
+
attrs = attrs.transform_keys(&:to_s)
|
|
57
|
+
@day = attrs["Day"]
|
|
58
|
+
@type = attrs["Type"]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_h
|
|
62
|
+
ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def ==(other)
|
|
66
|
+
other.is_a?(PaymentTerm) && day == other.day && type == other.type
|
|
67
|
+
end
|
|
68
|
+
alias eql? ==
|
|
69
|
+
|
|
70
|
+
def hash = [self.class, day, type].hash
|
|
71
|
+
|
|
72
|
+
def inspect
|
|
73
|
+
"#<#{self.class} day=#{day.inspect} type=#{type.inspect}>"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XeroKiwi
|
|
4
|
+
module Accounting
|
|
5
|
+
# A Xero phone number. Used by Organisation, Contact, and other resources.
|
|
6
|
+
#
|
|
7
|
+
# See: https://developer.xero.com/documentation/api/accounting/types#phones
|
|
8
|
+
class Phone
|
|
9
|
+
ATTRIBUTES = {
|
|
10
|
+
phone_type: "PhoneType",
|
|
11
|
+
phone_number: "PhoneNumber",
|
|
12
|
+
phone_area_code: "PhoneAreaCode",
|
|
13
|
+
phone_country_code: "PhoneCountryCode"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
attr_reader(*ATTRIBUTES.keys)
|
|
17
|
+
|
|
18
|
+
def initialize(attrs)
|
|
19
|
+
attrs = attrs.transform_keys(&:to_s)
|
|
20
|
+
@phone_type = attrs["PhoneType"]
|
|
21
|
+
@phone_number = attrs["PhoneNumber"]
|
|
22
|
+
@phone_area_code = attrs["PhoneAreaCode"]
|
|
23
|
+
@phone_country_code = attrs["PhoneCountryCode"]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def default? = phone_type == "DEFAULT"
|
|
27
|
+
def mobile? = phone_type == "MOBILE"
|
|
28
|
+
def fax? = phone_type == "FAX"
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def ==(other)
|
|
35
|
+
other.is_a?(Phone) && to_h == other.to_h
|
|
36
|
+
end
|
|
37
|
+
alias eql? ==
|
|
38
|
+
|
|
39
|
+
def hash = to_h.hash
|
|
40
|
+
|
|
41
|
+
def inspect
|
|
42
|
+
"#<#{self.class} type=#{phone_type.inspect} number=#{phone_number.inspect}>"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module XeroKiwi
|
|
6
|
+
module Accounting
|
|
7
|
+
# Represents a Xero Prepayment returned by the Accounting API.
|
|
8
|
+
#
|
|
9
|
+
# See: https://developer.xero.com/documentation/api/accounting/prepayments
|
|
10
|
+
class Prepayment
|
|
11
|
+
ATTRIBUTES = {
|
|
12
|
+
prepayment_id: "PrepaymentID",
|
|
13
|
+
type: "Type",
|
|
14
|
+
contact: "Contact",
|
|
15
|
+
date: "Date",
|
|
16
|
+
status: "Status",
|
|
17
|
+
line_amount_types: "LineAmountTypes",
|
|
18
|
+
line_items: "LineItems",
|
|
19
|
+
sub_total: "SubTotal",
|
|
20
|
+
total_tax: "TotalTax",
|
|
21
|
+
total: "Total",
|
|
22
|
+
updated_date_utc: "UpdatedDateUTC",
|
|
23
|
+
currency_code: "CurrencyCode",
|
|
24
|
+
currency_rate: "CurrencyRate",
|
|
25
|
+
invoice_number: "InvoiceNumber",
|
|
26
|
+
remaining_credit: "RemainingCredit",
|
|
27
|
+
allocations: "Allocations",
|
|
28
|
+
payments: "Payments",
|
|
29
|
+
has_attachments: "HasAttachments",
|
|
30
|
+
fully_paid_on_date: "FullyPaidOnDate"
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
attr_reader(*ATTRIBUTES.keys)
|
|
34
|
+
|
|
35
|
+
def self.from_response(payload)
|
|
36
|
+
return [] if payload.nil?
|
|
37
|
+
|
|
38
|
+
items = payload["Prepayments"]
|
|
39
|
+
return [] if items.nil?
|
|
40
|
+
|
|
41
|
+
items.map { |attrs| new(attrs) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def initialize(attrs, reference: false) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
45
|
+
attrs = attrs.transform_keys(&:to_s)
|
|
46
|
+
@is_reference = reference
|
|
47
|
+
@prepayment_id = attrs["PrepaymentID"]
|
|
48
|
+
@type = attrs["Type"]
|
|
49
|
+
@contact = attrs["Contact"] ? Contact.new(attrs["Contact"], reference: true) : nil
|
|
50
|
+
@date = parse_time(attrs["Date"])
|
|
51
|
+
@status = attrs["Status"]
|
|
52
|
+
@line_amount_types = attrs["LineAmountTypes"]
|
|
53
|
+
@line_items = (attrs["LineItems"] || []).map { |li| LineItem.new(li) }
|
|
54
|
+
@sub_total = attrs["SubTotal"]
|
|
55
|
+
@total_tax = attrs["TotalTax"]
|
|
56
|
+
@total = attrs["Total"]
|
|
57
|
+
@updated_date_utc = parse_time(attrs["UpdatedDateUTC"])
|
|
58
|
+
@currency_code = attrs["CurrencyCode"]
|
|
59
|
+
@currency_rate = attrs["CurrencyRate"]
|
|
60
|
+
@invoice_number = attrs["InvoiceNumber"]
|
|
61
|
+
@remaining_credit = attrs["RemainingCredit"]
|
|
62
|
+
@allocations = (attrs["Allocations"] || []).map { |a| Allocation.new(a) }
|
|
63
|
+
@payments = (attrs["Payments"] || []).map { |p| Payment.new(p, reference: true) }
|
|
64
|
+
@has_attachments = attrs["HasAttachments"]
|
|
65
|
+
@fully_paid_on_date = parse_time(attrs["FullyPaidOnDate"])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def reference? = @is_reference
|
|
69
|
+
|
|
70
|
+
def receive? = type == "RECEIVE-PREPAYMENT"
|
|
71
|
+
|
|
72
|
+
def spend? = type == "SPEND-PREPAYMENT"
|
|
73
|
+
|
|
74
|
+
def to_h
|
|
75
|
+
ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def ==(other)
|
|
79
|
+
other.is_a?(Prepayment) && other.prepayment_id == prepayment_id
|
|
80
|
+
end
|
|
81
|
+
alias eql? ==
|
|
82
|
+
|
|
83
|
+
def hash = [self.class, prepayment_id].hash
|
|
84
|
+
|
|
85
|
+
def inspect
|
|
86
|
+
"#<#{self.class} prepayment_id=#{prepayment_id.inspect} " \
|
|
87
|
+
"type=#{type.inspect} status=#{status.inspect} total=#{total.inspect}>"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def parse_time(value)
|
|
93
|
+
return nil if value.nil?
|
|
94
|
+
|
|
95
|
+
str = value.to_s.strip
|
|
96
|
+
return nil if str.empty?
|
|
97
|
+
|
|
98
|
+
if (match = str.match(%r{\A/Date\((\d+)([+-]\d{4})?\)/\z}))
|
|
99
|
+
Time.at(match[1].to_i / 1000.0).utc
|
|
100
|
+
else
|
|
101
|
+
str = "#{str}Z" unless str.match?(/[Zz]\z|[+-]\d{2}:?\d{2}\z/)
|
|
102
|
+
Time.iso8601(str)
|
|
103
|
+
end
|
|
104
|
+
rescue ArgumentError
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module XeroKiwi
|
|
4
|
+
module Accounting
|
|
5
|
+
# Represents a tracking category assignment on a line item or contact.
|
|
6
|
+
#
|
|
7
|
+
# See: https://developer.xero.com/documentation/api/accounting/invoices
|
|
8
|
+
class TrackingCategory
|
|
9
|
+
ATTRIBUTES = {
|
|
10
|
+
tracking_category_id: "TrackingCategoryID",
|
|
11
|
+
tracking_option_id: "TrackingOptionID",
|
|
12
|
+
name: "Name",
|
|
13
|
+
option: "Option"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
attr_reader(*ATTRIBUTES.keys)
|
|
17
|
+
|
|
18
|
+
def initialize(attrs)
|
|
19
|
+
attrs = attrs.transform_keys(&:to_s)
|
|
20
|
+
@tracking_category_id = attrs["TrackingCategoryID"]
|
|
21
|
+
@tracking_option_id = attrs["TrackingOptionID"]
|
|
22
|
+
@name = attrs["Name"]
|
|
23
|
+
@option = attrs["Option"]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
ATTRIBUTES.keys.to_h { |key| [key, public_send(key)] }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ==(other)
|
|
31
|
+
other.is_a?(TrackingCategory) && to_h == other.to_h
|
|
32
|
+
end
|
|
33
|
+
alias eql? ==
|
|
34
|
+
|
|
35
|
+
def hash = to_h.hash
|
|
36
|
+
|
|
37
|
+
def inspect
|
|
38
|
+
"#<#{self.class} name=#{name.inspect} option=#{option.inspect}>"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|