tlconnor-xero_gateway 1.0.3 → 1.0.4

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.
@@ -3,10 +3,35 @@ module XeroGateway
3
3
  include Dates
4
4
  include Money
5
5
 
6
+ class Error < RuntimeError; end
7
+ class NoGatewayError < Error; end
8
+
9
+ INVOICE_TYPE = {
10
+ 'ACCREC' => 'Accounts Receivable',
11
+ 'ACCPAY' => 'Accounts Payable'
12
+ } unless defined?(INVOICE_TYPE)
13
+
14
+ INVOICE_STATUS = {
15
+ 'AUTHORISED' => 'Approved invoices awaiting payment',
16
+ 'DELETED' => 'Draft invoices that are deleted',
17
+ 'DRAFT' => 'Invoices saved as draft or entered via API',
18
+ 'PAID' => 'Invoices approved and fully paid',
19
+ 'SUBMITTED' => 'Invoices entered by an employee awaiting approval',
20
+ 'VOID' => 'Approved invoices that are voided'
21
+ } unless defined?(INVOICE_STATUS)
22
+
23
+ # Xero::Gateway associated with this invoice.
24
+ attr_accessor :gateway
25
+
26
+ # Any errors that occurred when the #valid? method called.
27
+ attr_reader :errors
28
+
6
29
  # All accessible fields
7
- attr_accessor :invoice_id, :invoice_number, :invoice_type, :invoice_status, :date, :due_date, :reference, :tax_inclusive, :includes_tax, :sub_total, :total_tax, :total, :line_items, :contact
30
+ attr_accessor :invoice_id, :invoice_number, :invoice_type, :invoice_status, :date, :due_date, :reference, :tax_inclusive, :includes_tax, :line_items, :contact
8
31
 
9
32
  def initialize(params = {})
33
+ @errors ||= []
34
+
10
35
  params = {
11
36
  :contact => Contact.new,
12
37
  :date => Time.now,
@@ -15,13 +40,96 @@ module XeroGateway
15
40
  }.merge(params)
16
41
 
17
42
  params.each do |k,v|
18
- self.instance_variable_set("@#{k}", v) ## create and initialize an instance variable for this key/value pair
19
43
  self.send("#{k}=", v)
20
44
  end
21
45
 
22
46
  @line_items ||= []
23
- end
24
-
47
+ end
48
+
49
+ # Validate the Address record according to what will be valid by the gateway.
50
+ #
51
+ # Usage:
52
+ # address.valid? # Returns true/false
53
+ #
54
+ # Additionally sets address.errors array to an array of field/error.
55
+ def valid?
56
+ @errors = []
57
+
58
+ if !invoice_id.nil? && invoice_id !~ GUID_REGEX
59
+ @errors << ['invoice_id', 'must be blank or a valid Xero GUID']
60
+ end
61
+
62
+ if invoice_status && !INVOICE_STATUS[invoice_status]
63
+ @errors << ['invoice_status', "must be one of #{INVOICE_STATUS.keys.join('/')}"]
64
+ end
65
+
66
+ unless invoice_number
67
+ @errors << ['invoice_number', "can't be blank"]
68
+ end
69
+
70
+ unless date
71
+ @errors << ['invoice_date', "can't be blank"]
72
+ end
73
+
74
+ # Make sure contact is valid.
75
+ unless contact.valid?
76
+ @errors << ['contact', 'is invalid']
77
+ end
78
+
79
+ # Make sure all line_items are valid.
80
+ unless line_items.all? { | line_item | line_item.valid? }
81
+ @errors << ['line_items', "at least one line item invalid"]
82
+ end
83
+
84
+ @errors.size == 0
85
+ end
86
+
87
+ # Deprecated (but API for setter remains).
88
+ #
89
+ # As sub_total must equal SUM(line_item.line_amount) for the API call to pass, this is now
90
+ # automatically calculated in the sub_total method.
91
+ def sub_total=(value)
92
+ end
93
+
94
+ # Calculate the sub_total as the SUM(line_item.line_amount).
95
+ def sub_total
96
+ line_items.inject(BigDecimal.new('0')) { | sum, line_item | sum + BigDecimal.new(line_item.line_amount.to_s) }
97
+ end
98
+
99
+ # Deprecated (but API for setter remains).
100
+ #
101
+ # As total_tax must equal SUM(line_item.tax_amount) for the API call to pass, this is now
102
+ # automatically calculated in the total_tax method.
103
+ def total_tax=(value)
104
+ end
105
+
106
+ # Calculate the total_tax as the SUM(line_item.tax_amount).
107
+ def total_tax
108
+ line_items.inject(BigDecimal.new('0')) { | sum, line_item | sum + BigDecimal.new(line_item.tax_amount.to_s) }
109
+ end
110
+
111
+ # Deprecated (but API for setter remains).
112
+ #
113
+ # As total must equal sub_total + total_tax for the API call to pass, this is now
114
+ # automatically calculated in the total method.
115
+ def total=(value)
116
+ end
117
+
118
+ # Calculate the toal as sub_total + total_tax.
119
+ def total
120
+ sub_total + total_tax
121
+ end
122
+
123
+ # Helper method to check if the invoice is accounts payable.
124
+ def accounts_payable?
125
+ invoice_type == 'ACCPAY'
126
+ end
127
+
128
+ # Helper method to check if the invoice is accounts receivable.
129
+ def accounts_receivable?
130
+ invoice_type == 'ACCREC'
131
+ end
132
+
25
133
  def ==(other)
26
134
  ["invoice_number", "invoice_type", "invoice_status", "reference", "tax_inclusive", "includes_tax", "sub_total", "total_tax", "total", "contact", "line_items"].each do |field|
27
135
  return false if send(field) != other.send(field)
@@ -32,41 +140,28 @@ module XeroGateway
32
140
  return true
33
141
  end
34
142
 
143
+ # General purpose createsave method.
144
+ # If contact_id and contact_number are nil then create, otherwise, attempt to save.
145
+ def save
146
+ create
147
+ end
148
+
149
+ # Creates this invoice record (using gateway.create_invoice) with the associated gateway.
150
+ # If no gateway set, raise a Xero::Invoice::NoGatewayError exception.
151
+ def create
152
+ raise NoGatewayError unless gateway
153
+ gateway.create_invoice(self)
154
+ end
155
+
156
+ # Alias create as save as this is currently the only write action.
157
+ alias_method :save, :create
158
+
35
159
  def to_xml
36
160
  b = Builder::XmlMarkup.new
37
161
 
38
162
  b.Invoice {
39
163
  b.InvoiceType self.invoice_type
40
- b.Contact {
41
- b.ContactID self.contact.contact_id if self.contact.contact_id
42
- b.Name self.contact.name
43
- b.EmailAddress self.contact.email if self.contact.email
44
- b.Addresses {
45
- self.contact.addresses.each do |address|
46
- b.Address {
47
- b.AddressType address.address_type
48
- b.AddressLine1 address.line_1 if address.line_1
49
- b.AddressLine2 address.line_2 if address.line_2
50
- b.AddressLine3 address.line_3 if address.line_3
51
- b.AddressLine4 address.line_4 if address.line_4
52
- b.City address.city if address.city
53
- b.Region address.region if address.region
54
- b.PostalCode address.post_code if address.post_code
55
- b.Country address.country if address.country
56
- }
57
- end
58
- }
59
- b.Phones {
60
- self.contact.phones.each do |phone|
61
- b.Phone {
62
- b.PhoneType phone.phone_type
63
- b.PhoneNumber phone.number
64
- b.PhoneAreaCode phone.area_code if phone.area_code
65
- b.PhoneCountryCode phone.country_code if phone.country_code
66
- }
67
- end
68
- }
69
- }
164
+ contact.to_xml(b)
70
165
  b.InvoiceDate Invoice.format_date_time(self.date)
71
166
  b.DueDate Invoice.format_date_time(self.due_date) if self.due_date
72
167
  b.InvoiceNumber self.invoice_number
@@ -78,21 +173,7 @@ module XeroGateway
78
173
  b.Total Invoice.format_money(self.total) if self.total
79
174
  b.LineItems {
80
175
  self.line_items.each do |line_item|
81
- b.LineItem {
82
- b.Description line_item.description
83
- b.Quantity line_item.quantity if line_item.quantity
84
- b.UnitAmount Invoice.format_money(line_item.unit_amount)
85
- b.TaxType line_item.tax_type if line_item.tax_type
86
- b.TaxAmount Invoice.format_money(line_item.tax_amount) if line_item.tax_amount
87
- b.LineAmount Invoice.format_money(line_item.line_amount)
88
- b.AccountCode line_item.account_code || 200
89
- b.Tracking {
90
- b.TrackingCategory {
91
- b.Name line_item.tracking_category
92
- b.Option line_item.tracking_option
93
- }
94
- }
95
- }
176
+ line_item.to_xml(b)
96
177
  end
97
178
  }
98
179
  }
@@ -115,34 +196,10 @@ module XeroGateway
115
196
  when "TotalTax" then invoice.total_tax = BigDecimal.new(element.text)
116
197
  when "Total" then invoice.total = BigDecimal.new(element.text)
117
198
  when "Contact" then invoice.contact = Contact.from_xml(element)
118
- when "LineItems" then element.children.each {|line_item| invoice.line_items << parse_line_item(line_item)}
199
+ when "LineItems" then element.children.each {|line_item| invoice.line_items << LineItem.from_xml(line_item)}
119
200
  end
120
201
  end
121
202
  invoice
122
- end
123
-
124
- private
125
-
126
- def self.parse_line_item(line_item_element)
127
- line_item = LineItem.new
128
- line_item_element.children.each do |element|
129
- case(element.name)
130
- when "LineItemID" then line_item.line_item_id = element.text
131
- when "Description" then line_item.description = element.text
132
- when "Quantity" then line_item.quantity = element.text.to_i
133
- when "UnitAmount" then line_item.unit_amount = BigDecimal.new(element.text)
134
- when "TaxType" then line_item.tax_type = element.text
135
- when "TaxAmount" then line_item.tax_amount = BigDecimal.new(element.text)
136
- when "LineAmount" then line_item.line_amount = BigDecimal.new(element.text)
137
- when "AccountCode" then line_item.account_code = element.text
138
- when "Tracking" then
139
- if element.elements['TrackingCategory']
140
- line_item.tracking_category = element.elements['TrackingCategory/Name'].text
141
- line_item.tracking_option = element.elements['TrackingCategory/Option'].text
142
- end
143
- end
144
- end
145
- line_item
146
- end
203
+ end
147
204
  end
148
205
  end
@@ -1,17 +1,103 @@
1
+ require File.join(File.dirname(__FILE__), 'account')
2
+
1
3
  module XeroGateway
2
4
  class LineItem
3
- # All accessible fields
4
- attr_accessor :line_item_id, :description, :quantity, :unit_amount, :tax_type, :tax_amount, :line_amount, :account_code, :tracking_category, :tracking_option
5
+ include Money
5
6
 
7
+ TAX_TYPE = Account::TAX_TYPE unless defined?(TAX_TYPE)
8
+
9
+ # Any errors that occurred when the #valid? method called.
10
+ attr_reader :errors
11
+
12
+ # All accessible fields
13
+ attr_accessor :line_item_id, :description, :quantity, :unit_amount, :tax_type, :tax_amount, :account_code, :tracking_category, :tracking_option
14
+
6
15
  def initialize(params = {})
7
- params = {
8
- :quantity => 1
9
- }.merge(params)
16
+ @errors ||= []
17
+ @quantity = 1
18
+ @unit_amount = BigDecimal.new('0')
19
+ @tax_amount = BigDecimal.new('0')
10
20
 
11
21
  params.each do |k,v|
12
- self.instance_variable_set("@#{k}", v) ## create and initialize an instance variable for this key/value pair
13
22
  self.send("#{k}=", v)
14
23
  end
24
+ end
25
+
26
+ # Validate the LineItem record according to what will be valid by the gateway.
27
+ #
28
+ # Usage:
29
+ # line_item.valid? # Returns true/false
30
+ #
31
+ # Additionally sets line_item.errors array to an array of field/error.
32
+ def valid?
33
+ @errors = []
34
+
35
+ if !line_item_id.nil? && line_item_id !~ GUID_REGEX
36
+ @errors << ['line_item_id', 'must be blank or a valid Xero GUID']
37
+ end
38
+
39
+ unless description
40
+ @errors << ['description', "can't be blank"]
41
+ end
42
+
43
+ if tax_type && !TAX_TYPE[tax_type]
44
+ @errors << ['tax_type', "must be one of #{TAX_TYPE.keys.join('/')}"]
45
+ end
46
+
47
+ @errors.size == 0
48
+ end
49
+
50
+ # Deprecated (but API for setter remains).
51
+ #
52
+ # As line_amount must equal quantity * unit_amount for the API call to pass, this is now
53
+ # automatically calculated in the line_amount method.
54
+ def line_amount=(value)
55
+ end
56
+
57
+ # Calculate the line_amount as quantity * unit_amount as this value must be correct
58
+ # for the API call to succeed.
59
+ def line_amount
60
+ quantity * unit_amount
61
+ end
62
+
63
+ def to_xml(b = Builder::XmlMarkup.new)
64
+ b.LineItem {
65
+ b.Description description
66
+ b.Quantity quantity if quantity
67
+ b.UnitAmount LineItem.format_money(unit_amount)
68
+ b.TaxType tax_type if tax_type
69
+ b.TaxAmount LineItem.format_money(tax_amount) if tax_amount
70
+ b.LineAmount LineItem.format_money(line_amount)
71
+ b.AccountCode account_code if account_code
72
+ b.Tracking {
73
+ b.TrackingCategory {
74
+ b.Name tracking_category
75
+ b.Option tracking_option
76
+ }
77
+ }
78
+ }
79
+ end
80
+
81
+ def self.from_xml(line_item_element)
82
+ line_item = LineItem.new
83
+ line_item_element.children.each do |element|
84
+ case(element.name)
85
+ when "LineItemID" then line_item.line_item_id = element.text
86
+ when "Description" then line_item.description = element.text
87
+ when "Quantity" then line_item.quantity = element.text.to_i
88
+ when "UnitAmount" then line_item.unit_amount = BigDecimal.new(element.text)
89
+ when "TaxType" then line_item.tax_type = element.text
90
+ when "TaxAmount" then line_item.tax_amount = BigDecimal.new(element.text)
91
+ when "LineAmount" then line_item.line_amount = BigDecimal.new(element.text)
92
+ when "AccountCode" then line_item.account_code = element.text
93
+ when "Tracking" then
94
+ if element.elements['TrackingCategory']
95
+ line_item.tracking_category = element.elements['TrackingCategory/Name'].text
96
+ line_item.tracking_option = element.elements['TrackingCategory/Option'].text
97
+ end
98
+ end
99
+ end
100
+ line_item
15
101
  end
16
102
 
17
103
  def ==(other)
@@ -21,5 +107,5 @@ module XeroGateway
21
107
  end
22
108
  return true
23
109
  end
24
- end
110
+ end
25
111
  end
@@ -1,18 +1,72 @@
1
1
  module XeroGateway
2
2
  class Phone
3
+
4
+ PHONE_TYPE = {
5
+ 'DEFAULT' => 'Default',
6
+ 'DDI' => 'Direct Dial-In',
7
+ 'MOBILE' => 'Mobile',
8
+ 'FAX' => 'Fax'
9
+ } unless defined?(PHONE_TYPE)
10
+
11
+ # Any errors that occurred when the #valid? method called.
12
+ attr_reader :errors
13
+
3
14
  attr_accessor :phone_type, :number, :area_code, :country_code
4
15
 
5
16
  def initialize(params = {})
17
+ @errors ||= []
18
+
6
19
  params = {
7
20
  :phone_type => "DEFAULT"
8
21
  }.merge(params)
9
22
 
10
23
  params.each do |k,v|
11
- self.instance_variable_set("@#{k}", v) ## create and initialize an instance variable for this key/value pair
12
24
  self.send("#{k}=", v)
13
25
  end
14
26
  end
15
27
 
28
+ # Validate the Phone record according to what will be valid by the gateway.
29
+ #
30
+ # Usage:
31
+ # phone.valid? # Returns true/false
32
+ #
33
+ # Additionally sets phone.errors array to an array of field/error.
34
+ def valid?
35
+ @errors = []
36
+
37
+ unless number
38
+ @errors << ['number', "can't be blank"]
39
+ end
40
+
41
+ if phone_type && !PHONE_TYPE[phone_type]
42
+ @errors << ['phone_type', "must be one of #{PHONE_TYPE.keys.join('/')}"]
43
+ end
44
+
45
+ @errors.size == 0
46
+ end
47
+
48
+ def to_xml(b = Builder::XmlMarkup.new)
49
+ b.Phone {
50
+ b.PhoneType phone_type
51
+ b.PhoneNumber number
52
+ b.PhoneAreaCode area_code if area_code
53
+ b.PhoneCountryCode country_code if country_code
54
+ }
55
+ end
56
+
57
+ def self.from_xml(phone_element)
58
+ phone = Phone.new
59
+ phone_element.children.each do |element|
60
+ case(element.name)
61
+ when "PhoneType" then phone.phone_type = element.text
62
+ when "PhoneNumber" then phone.number = element.text
63
+ when "PhoneAreaCode" then phone.area_code = element.text
64
+ when "PhoneCountryCode" then phone.country_code = element.text
65
+ end
66
+ end
67
+ phone
68
+ end
69
+
16
70
  def ==(other)
17
71
  [:phone_type, :number, :area_code, :country_code].each do |field|
18
72
  return false if send(field) != other.send(field)
@@ -20,4 +74,4 @@ module XeroGateway
20
74
  return true
21
75
  end
22
76
  end
23
- end
77
+ end