xeroizer 2.15.3 → 2.15.5
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -0
- data/Gemfile.lock +2 -0
- data/LICENSE.txt +22 -1
- data/README.md +29 -2
- data/Rakefile +2 -2
- data/VERSION +1 -1
- data/lib/xeroizer.rb +0 -1
- data/lib/xeroizer/exceptions.rb +17 -2
- data/lib/xeroizer/http.rb +21 -4
- data/lib/xeroizer/models/bank_transaction.rb +17 -5
- data/lib/xeroizer/models/contact.rb +17 -17
- data/lib/xeroizer/models/credit_note.rb +18 -18
- data/lib/xeroizer/models/invoice.rb +54 -32
- data/lib/xeroizer/models/line_item.rb +6 -4
- data/lib/xeroizer/models/manual_journal.rb +12 -12
- data/lib/xeroizer/models/payment.rb +11 -11
- data/lib/xeroizer/models/tax_rate.rb +1 -0
- data/lib/xeroizer/oauth.rb +24 -5
- data/lib/xeroizer/record/base.rb +25 -1
- data/lib/xeroizer/record/base_model.rb +84 -11
- data/lib/xeroizer/record/base_model_http_proxy.rb +2 -1
- data/lib/xeroizer/record/model_definition_helper.rb +3 -0
- data/lib/xeroizer/record/xml_helper.rb +6 -2
- data/lib/xeroizer/response.rb +14 -0
- data/test/acceptance/acceptance_test.rb +1 -1
- data/test/acceptance/bulk_operations_test.rb +48 -0
- data/test/unit/models/bank_transaction_model_parsing_test.rb +3 -1
- data/test/unit/models/invoice_test.rb +13 -12
- data/test/unit/record/base_model_test.rb +8 -0
- data/xeroizer.gemspec +7 -3
- metadata +63 -48
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
data/LICENSE.txt
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Copyright (c)
|
1
|
+
Copyright (c) 2013 Wayne Robinson
|
2
2
|
|
3
3
|
Permission is hereby granted, free of charge, to any person obtaining
|
4
4
|
a copy of this software and associated documentation files (the
|
@@ -18,3 +18,24 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
18
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
19
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
20
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
-----------------------------------------------------------------------
|
23
|
+
|
24
|
+
Part of the HTTP communication and OAuth authentication library were
|
25
|
+
provided by the https://github.com/tlconnor/xero_gateway gem created by
|
26
|
+
Tim Connor (github.com/tlconnor) and Nik Wakelin (github.com/nikz).
|
27
|
+
The license for this Gem is included below:
|
28
|
+
|
29
|
+
Copyright (c) 2008 Tim Connor <tlconnor@gmail.com>
|
30
|
+
|
31
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
32
|
+
purpose with or without fee is hereby granted, provided that the above
|
33
|
+
copyright notice and this permission notice appear in all copies.
|
34
|
+
|
35
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
36
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
37
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
38
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
39
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
40
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
41
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
data/README.md
CHANGED
@@ -6,7 +6,7 @@ Xeroizer API Library ![Project status](http://stillmaintained.com/waynerobinson/
|
|
6
6
|
**Github**: [https://github.com/waynerobinson/xeroizer](https://github.com/waynerobinson/xeroizer)
|
7
7
|
**Author**: Wayne Robinson [http://www.wayne-robinson.com](http://www.wayne-robinson.com)
|
8
8
|
**Contributors**: See Contributors section below
|
9
|
-
**Copyright**: 2007-
|
9
|
+
**Copyright**: 2007-2013
|
10
10
|
**License**: MIT License
|
11
11
|
|
12
12
|
Introduction
|
@@ -435,9 +435,30 @@ contact.save
|
|
435
435
|
Have a look at the models in `lib/xeroizer/models/` to see the valid attributes, associations and
|
436
436
|
minimum validation requirements for each of the record types.
|
437
437
|
|
438
|
+
### Bulk Creates & Updates
|
439
|
+
|
440
|
+
Xero has a hard daily limit on the number of API requests you can make (currently 1,000 requests
|
441
|
+
per account per day). To save on requests, you can batch creates and updates into a single PUT or
|
442
|
+
POST call, like so:
|
443
|
+
|
444
|
+
```ruby
|
445
|
+
contact1 = xero.Contact.create(some_attributes)
|
446
|
+
xero.Contact.batch_save do
|
447
|
+
contact1.email_address = "foo@bar.com"
|
448
|
+
contact2 = xero.Contact.build(some_other_attributes)
|
449
|
+
contact3 = xero.Contact.build(some_more_attributes)
|
450
|
+
end
|
451
|
+
```
|
452
|
+
|
453
|
+
`batch_save` will issue one PUT request for every 2,000 unsaved records built within its block, and one
|
454
|
+
POST request for evert 2,000 existing records that have been altered within its block. If any of the
|
455
|
+
unsaved records aren't valid, it'll return `false` before sending anything across the wire;
|
456
|
+
otherwise, it returns `true`. `batch_save` takes one optional argument: the number of records to
|
457
|
+
create/update per request. (Defaults to 2,000.)
|
458
|
+
|
438
459
|
### Errors
|
439
460
|
|
440
|
-
If a record doesn't match
|
461
|
+
If a record doesn't match its internal validation requirements, the `#save` method will return
|
441
462
|
`false` and the `#errors` attribute will be populated with what went wrong.
|
442
463
|
|
443
464
|
For example:
|
@@ -527,3 +548,9 @@ client = Xeroizer::PublicApplication.new(YOUR_OAUTH_CONSUMER_KEY,
|
|
527
548
|
YOUR_OAUTH_CONSUMER_SECRET,
|
528
549
|
:rate_limit_sleep => 2)
|
529
550
|
```
|
551
|
+
|
552
|
+
|
553
|
+
### Contributors
|
554
|
+
Xeroizer was inspired by the https://github.com/tlconnor/xero_gateway gem created by Tim Connor
|
555
|
+
and Nik Wakelin and portions of the networking and authentication code are based completely off
|
556
|
+
this project. Copyright for these components remains held in the name of Tim Connor.
|
data/Rakefile
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'rake'
|
2
2
|
require 'rake/testtask'
|
3
|
-
require '
|
3
|
+
require 'rdoc/task'
|
4
4
|
require 'rubygems'
|
5
5
|
require 'yard'
|
6
6
|
|
@@ -52,4 +52,4 @@ end
|
|
52
52
|
YARD::Rake::YardocTask.new do |t|
|
53
53
|
# t.files = ['lib/**/*.rb', OTHER_PATHS] # optional
|
54
54
|
# t.options = ['--any', '--extra', '--opts'] # optional
|
55
|
-
end
|
55
|
+
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.15.
|
1
|
+
2.15.5
|
data/lib/xeroizer.rb
CHANGED
data/lib/xeroizer/exceptions.rb
CHANGED
@@ -1,12 +1,27 @@
|
|
1
|
+
# Copyright (c) 2008 Tim Connor <tlconnor@gmail.com>
|
2
|
+
#
|
3
|
+
# Permission to use, copy, modify, and/or distribute this software for any
|
4
|
+
# purpose with or without fee is hereby granted, provided that the above
|
5
|
+
# copyright notice and this permission notice appear in all copies.
|
6
|
+
#
|
7
|
+
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
8
|
+
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
9
|
+
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
10
|
+
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
11
|
+
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
12
|
+
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
13
|
+
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
14
|
+
|
1
15
|
module Xeroizer
|
2
16
|
class ApiException < StandardError
|
3
17
|
|
4
|
-
attr_reader :type, :message, :xml, :request_body
|
18
|
+
attr_reader :type, :message, :xml, :parsed_xml, :request_body
|
5
19
|
|
6
|
-
def initialize(type, message, xml, request_body)
|
20
|
+
def initialize(type, message, xml, parsed_xml, request_body)
|
7
21
|
@type = type
|
8
22
|
@message = message
|
9
23
|
@xml = xml
|
24
|
+
@parsed_xml = parsed_xml
|
10
25
|
@request_body = request_body
|
11
26
|
end
|
12
27
|
|
data/lib/xeroizer/http.rb
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
# Copyright (c) 2008 Tim Connor <tlconnor@gmail.com>
|
2
|
+
#
|
3
|
+
# Permission to use, copy, modify, and/or distribute this software for any
|
4
|
+
# purpose with or without fee is hereby granted, provided that the above
|
5
|
+
# copyright notice and this permission notice appear in all copies.
|
6
|
+
#
|
7
|
+
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
8
|
+
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
9
|
+
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
10
|
+
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
11
|
+
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
12
|
+
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
13
|
+
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
14
|
+
|
1
15
|
module Xeroizer
|
2
16
|
module Http
|
3
17
|
|
@@ -69,7 +83,7 @@ module Xeroizer
|
|
69
83
|
|
70
84
|
begin
|
71
85
|
attempts += 1
|
72
|
-
logger.info("\n== [#{Time.now.to_s}] XeroGateway Request: #{uri.request_uri} ") if self.logger
|
86
|
+
logger.info("\n== [#{Time.now.to_s}] XeroGateway Request: #{method.to_s.upcase} #{uri.request_uri} ") if self.logger
|
73
87
|
|
74
88
|
response = case method
|
75
89
|
when :get then client.get(uri.request_uri, headers)
|
@@ -94,6 +108,8 @@ module Xeroizer
|
|
94
108
|
handle_oauth_error!(response)
|
95
109
|
when 404
|
96
110
|
handle_object_not_found!(response, url)
|
111
|
+
when 503
|
112
|
+
handle_oauth_error!(response)
|
97
113
|
else
|
98
114
|
raise "Unknown response code: #{response.code.to_i}"
|
99
115
|
end
|
@@ -131,16 +147,17 @@ module Xeroizer
|
|
131
147
|
|
132
148
|
# XeroGenericApplication API Exceptions *claim* to be UTF-16 encoded, but fail REXML/Iconv parsing...
|
133
149
|
# So let's ignore that :)
|
134
|
-
|
150
|
+
raw_response.gsub! '<?xml version="1.0" encoding="utf-16"?>', ''
|
135
151
|
|
136
152
|
# doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all)
|
137
153
|
doc = Nokogiri::XML(raw_response)
|
138
154
|
|
139
155
|
if doc && doc.root && doc.root.name == "ApiException"
|
140
156
|
|
141
|
-
raise ApiException.new(doc.root.xpath("Type").text,
|
142
|
-
doc.root.xpath("Message").text,
|
157
|
+
raise ApiException.new(doc.root.xpath("Type").text,
|
158
|
+
doc.root.xpath("Message").text,
|
143
159
|
raw_response,
|
160
|
+
doc,
|
144
161
|
request_body)
|
145
162
|
|
146
163
|
else
|
@@ -8,20 +8,31 @@ module Xeroizer
|
|
8
8
|
end
|
9
9
|
|
10
10
|
class BankTransaction < Base
|
11
|
+
|
12
|
+
BANK_TRANSACTION_STATUS = {
|
13
|
+
'ACTIVE' => 'Active bank transactions',
|
14
|
+
'DELETED' => 'Deleted bank transactions',
|
15
|
+
} unless defined?(BANK_TRANSACTION_STATUS)
|
16
|
+
BANK_TRANSACTION_STATUSES = BANK_TRANSACTION_STATUS.keys.sort
|
17
|
+
|
18
|
+
|
11
19
|
def initialize(parent)
|
12
20
|
super parent
|
13
21
|
self.line_amount_types = "Exclusive"
|
14
22
|
end
|
15
23
|
|
16
24
|
set_primary_key :bank_transaction_id
|
25
|
+
list_contains_summary_only true
|
26
|
+
|
17
27
|
string :type
|
18
28
|
date :date
|
19
29
|
|
20
|
-
|
21
|
-
date
|
22
|
-
string
|
23
|
-
string
|
24
|
-
boolean
|
30
|
+
datetime_utc :updated_date_utc, :api_name => "UpdatedDateUTC"
|
31
|
+
date :fully_paid_on_date
|
32
|
+
string :reference
|
33
|
+
string :bank_transaction_id, :api_name => "BankTransactionID"
|
34
|
+
boolean :is_reconciled
|
35
|
+
string :status
|
25
36
|
|
26
37
|
alias_method :reconciled?, :is_reconciled
|
27
38
|
|
@@ -36,6 +47,7 @@ module Xeroizer
|
|
36
47
|
validates_inclusion_of :type,
|
37
48
|
:in => %w{SPEND RECEIVE}, :allow_blanks => false,
|
38
49
|
:message => "Invalid type. Expected either SPEND or RECEIVE."
|
50
|
+
validates_inclusion_of :status, :in => BANK_TRANSACTION_STATUSES, :unless => :new_record?
|
39
51
|
|
40
52
|
validates_presence_of :contact, :bank_account, :allow_blanks => false
|
41
53
|
|
@@ -18,23 +18,23 @@ module Xeroizer
|
|
18
18
|
set_possible_primary_keys :contact_id, :contact_number
|
19
19
|
list_contains_summary_only true
|
20
20
|
|
21
|
-
guid
|
22
|
-
string
|
23
|
-
string
|
24
|
-
string
|
25
|
-
string
|
26
|
-
string
|
27
|
-
string
|
28
|
-
string
|
29
|
-
string
|
30
|
-
string
|
31
|
-
string
|
32
|
-
string
|
33
|
-
string
|
34
|
-
string
|
35
|
-
|
36
|
-
boolean
|
37
|
-
boolean
|
21
|
+
guid :contact_id
|
22
|
+
string :contact_number
|
23
|
+
string :contact_status
|
24
|
+
string :name
|
25
|
+
string :tax_number
|
26
|
+
string :bank_account_details
|
27
|
+
string :accounts_receivable_tax_type
|
28
|
+
string :accounts_payable_tax_type
|
29
|
+
string :first_name
|
30
|
+
string :last_name
|
31
|
+
string :email_address
|
32
|
+
string :skype_user_name
|
33
|
+
string :contact_groups
|
34
|
+
string :default_currency
|
35
|
+
datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
|
36
|
+
boolean :is_supplier
|
37
|
+
boolean :is_customer
|
38
38
|
|
39
39
|
has_many :addresses, :list_complete => true
|
40
40
|
has_many :phones, :list_complete => true
|
@@ -44,24 +44,24 @@ module Xeroizer
|
|
44
44
|
set_possible_primary_keys :credit_note_id, :credit_note_number
|
45
45
|
list_contains_summary_only true
|
46
46
|
|
47
|
-
guid
|
48
|
-
string
|
49
|
-
string
|
50
|
-
string
|
51
|
-
date
|
52
|
-
date
|
53
|
-
string
|
54
|
-
string
|
55
|
-
decimal
|
56
|
-
decimal
|
57
|
-
decimal
|
58
|
-
|
59
|
-
string
|
60
|
-
datetime
|
61
|
-
boolean
|
47
|
+
guid :credit_note_id
|
48
|
+
string :credit_note_number
|
49
|
+
string :reference
|
50
|
+
string :type
|
51
|
+
date :date
|
52
|
+
date :due_date
|
53
|
+
string :status
|
54
|
+
string :line_amount_types
|
55
|
+
decimal :sub_total, :calculated => true
|
56
|
+
decimal :total_tax, :calculated => true
|
57
|
+
decimal :total, :calculated => true
|
58
|
+
datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
|
59
|
+
string :currency_code
|
60
|
+
datetime :fully_paid_on_date
|
61
|
+
boolean :sent_to_contact
|
62
62
|
|
63
|
-
belongs_to
|
64
|
-
has_many
|
63
|
+
belongs_to :contact
|
64
|
+
has_many :line_items
|
65
65
|
|
66
66
|
validates_inclusion_of :type, :in => CREDIT_NOTE_TYPES
|
67
67
|
validates_inclusion_of :status, :in => CREDIT_NOTE_STATUSES, :allow_blanks => true
|
@@ -127,4 +127,4 @@ module Xeroizer
|
|
127
127
|
end
|
128
128
|
|
129
129
|
end
|
130
|
-
end
|
130
|
+
end
|
@@ -2,7 +2,10 @@ module Xeroizer
|
|
2
2
|
module Record
|
3
3
|
|
4
4
|
class InvoiceModel < BaseModel
|
5
|
-
|
5
|
+
# To create a new invoice, use the folowing
|
6
|
+
# $xero_client.Invoice.build(type: 'ACCREC', ..., contact: {name: 'Foo Bar'},...)
|
7
|
+
# Note that we are not making an api request to xero just to get the contact
|
8
|
+
|
6
9
|
set_permissions :read, :write, :update
|
7
10
|
|
8
11
|
public
|
@@ -44,31 +47,31 @@ module Xeroizer
|
|
44
47
|
set_possible_primary_keys :invoice_id, :invoice_number
|
45
48
|
list_contains_summary_only true
|
46
49
|
|
47
|
-
guid
|
48
|
-
string
|
49
|
-
string
|
50
|
-
guid
|
51
|
-
string
|
52
|
-
string
|
53
|
-
date
|
54
|
-
date
|
55
|
-
string
|
56
|
-
string
|
57
|
-
decimal
|
58
|
-
decimal
|
59
|
-
decimal
|
60
|
-
decimal
|
61
|
-
decimal
|
62
|
-
decimal
|
63
|
-
|
64
|
-
string
|
65
|
-
datetime
|
66
|
-
boolean
|
50
|
+
guid :invoice_id
|
51
|
+
string :invoice_number
|
52
|
+
string :reference
|
53
|
+
guid :branding_theme_id
|
54
|
+
string :url
|
55
|
+
string :type
|
56
|
+
date :date
|
57
|
+
date :due_date
|
58
|
+
string :status
|
59
|
+
string :line_amount_types
|
60
|
+
decimal :sub_total, :calculated => true
|
61
|
+
decimal :total_tax, :calculated => true
|
62
|
+
decimal :total, :calculated => true
|
63
|
+
decimal :amount_due
|
64
|
+
decimal :amount_paid
|
65
|
+
decimal :amount_credited
|
66
|
+
datetime_utc :updated_date_utc, :api_name => 'UpdatedDateUTC'
|
67
|
+
string :currency_code
|
68
|
+
datetime :fully_paid_on_date
|
69
|
+
boolean :sent_to_contact
|
67
70
|
|
68
|
-
belongs_to
|
69
|
-
has_many
|
70
|
-
has_many
|
71
|
-
has_many
|
71
|
+
belongs_to :contact
|
72
|
+
has_many :line_items
|
73
|
+
has_many :payments
|
74
|
+
has_many :credit_notes
|
72
75
|
|
73
76
|
validates_presence_of :date, :due_date, :unless => :new_record?
|
74
77
|
validates_inclusion_of :type, :in => INVOICE_TYPES
|
@@ -107,14 +110,24 @@ module Xeroizer
|
|
107
110
|
type == 'ACCREC'
|
108
111
|
end
|
109
112
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
113
|
+
def sub_total=(sub_total)
|
114
|
+
@sub_total_is_set = true
|
115
|
+
attributes[:sub_total] = sub_total
|
116
|
+
end
|
117
|
+
|
118
|
+
def total_tax=(total_tax)
|
119
|
+
@total_tax_is_set = true
|
120
|
+
attributes[:total_tax] = total_tax
|
121
|
+
end
|
122
|
+
|
123
|
+
def total=(total)
|
124
|
+
@total_is_set = true
|
125
|
+
attributes[:total] = total
|
126
|
+
end
|
114
127
|
|
115
128
|
# Calculate sub_total from line_items.
|
116
129
|
def sub_total(always_summary = false)
|
117
|
-
if
|
130
|
+
if !@sub_total_is_set && not_summary_or_loaded_record(always_summary)
|
118
131
|
sum = (line_items || []).inject(BigDecimal.new('0')) { | sum, line_item | sum + line_item.line_amount }
|
119
132
|
|
120
133
|
# If the default amount types are inclusive of 'tax' then remove the tax amount from this sub-total.
|
@@ -127,7 +140,7 @@ module Xeroizer
|
|
127
140
|
|
128
141
|
# Calculate total_tax from line_items.
|
129
142
|
def total_tax(always_summary = false)
|
130
|
-
if
|
143
|
+
if !@total_tax_is_set && not_summary_or_loaded_record(always_summary)
|
131
144
|
(line_items || []).inject(BigDecimal.new('0')) { | sum, line_item | sum + line_item.tax_amount }
|
132
145
|
else
|
133
146
|
attributes[:total_tax]
|
@@ -136,13 +149,22 @@ module Xeroizer
|
|
136
149
|
|
137
150
|
# Calculate the total from line_items.
|
138
151
|
def total(always_summary = false)
|
139
|
-
|
152
|
+
if !@total_is_set && not_summary_or_loaded_record(always_summary)
|
140
153
|
sub_total + total_tax
|
141
154
|
else
|
142
155
|
attributes[:total]
|
143
156
|
end
|
144
157
|
end
|
145
158
|
|
159
|
+
def not_summary_or_loaded_record(always_summary)
|
160
|
+
!always_summary && loaded_record?
|
161
|
+
end
|
162
|
+
|
163
|
+
def loaded_record?
|
164
|
+
new_record? ||
|
165
|
+
(!new_record? && line_items && line_items.size > 0)
|
166
|
+
end
|
167
|
+
|
146
168
|
# Retrieve the PDF version of this invoice.
|
147
169
|
# @param [String] filename optional filename to store the PDF in instead of returning the data.
|
148
170
|
def pdf(filename = nil)
|