tlconnor-xero_gateway 1.0.3 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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