jm81-qbfc 0.3.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.
- data/.gitignore +11 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +85 -0
- data/Rakefile +81 -0
- data/VERSION +1 -0
- data/init.rb +3 -0
- data/install.rb +1 -0
- data/lib/qbfc.rb +41 -0
- data/lib/qbfc/base.rb +82 -0
- data/lib/qbfc/element.rb +243 -0
- data/lib/qbfc/entities/generated.rb +8 -0
- data/lib/qbfc/entity.rb +11 -0
- data/lib/qbfc/info.rb +42 -0
- data/lib/qbfc/infos/generated.rb +9 -0
- data/lib/qbfc/item.rb +29 -0
- data/lib/qbfc/items/generated.rb +11 -0
- data/lib/qbfc/list.rb +84 -0
- data/lib/qbfc/lists/account.rb +24 -0
- data/lib/qbfc/lists/generated.rb +15 -0
- data/lib/qbfc/lists/qb_class.rb +25 -0
- data/lib/qbfc/modifiable.rb +31 -0
- data/lib/qbfc/ole_wrapper.rb +201 -0
- data/lib/qbfc/qb_collection.rb +26 -0
- data/lib/qbfc/qb_types.rb +18 -0
- data/lib/qbfc/qbfc_const.rb +14 -0
- data/lib/qbfc/report.rb +95 -0
- data/lib/qbfc/reports/aging.rb +13 -0
- data/lib/qbfc/reports/budget_summary.rb +13 -0
- data/lib/qbfc/reports/custom_detail.rb +9 -0
- data/lib/qbfc/reports/custom_summary.rb +9 -0
- data/lib/qbfc/reports/general_detail.rb +44 -0
- data/lib/qbfc/reports/general_summary.rb +33 -0
- data/lib/qbfc/reports/job.rb +14 -0
- data/lib/qbfc/reports/payroll_detail.rb +13 -0
- data/lib/qbfc/reports/payroll_summary.rb +13 -0
- data/lib/qbfc/reports/rows.rb +51 -0
- data/lib/qbfc/reports/time.rb +12 -0
- data/lib/qbfc/request.rb +295 -0
- data/lib/qbfc/session.rb +147 -0
- data/lib/qbfc/terms.rb +10 -0
- data/lib/qbfc/terms/generated.rb +10 -0
- data/lib/qbfc/transaction.rb +110 -0
- data/lib/qbfc/transactions/generated.rb +25 -0
- data/lib/qbfc/voidable.rb +11 -0
- data/spec/fixtures/test.lgb +0 -0
- data/spec/fixtures/test.qbw +0 -0
- data/spec/fixtures/test.qbw.TLG +0 -0
- data/spec/integration/add_spec.rb +31 -0
- data/spec/integration/base_spec.rb +18 -0
- data/spec/integration/belongs_to_spec.rb +64 -0
- data/spec/integration/company_spec.rb +30 -0
- data/spec/integration/conditions_spec.rb +59 -0
- data/spec/integration/customer_spec.rb +46 -0
- data/spec/integration/element_finders_spec.rb +20 -0
- data/spec/integration/quick_test.rb +31 -0
- data/spec/integration/request_options_spec.rb +68 -0
- data/spec/rcov.opts +1 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +62 -0
- data/spec/unit/base_spec.rb +138 -0
- data/spec/unit/element_finder_spec.rb +185 -0
- data/spec/unit/element_spec.rb +108 -0
- data/spec/unit/entities/generated_spec.rb +18 -0
- data/spec/unit/entity_spec.rb +18 -0
- data/spec/unit/info/generated_spec.rb +12 -0
- data/spec/unit/info_spec.rb +48 -0
- data/spec/unit/item_spec.rb +33 -0
- data/spec/unit/items/generated_spec.rb +16 -0
- data/spec/unit/list_finders_spec.rb +129 -0
- data/spec/unit/list_spec.rb +86 -0
- data/spec/unit/lists/account_spec.rb +20 -0
- data/spec/unit/lists/generated_spec.rb +15 -0
- data/spec/unit/lists/qb_class_spec.rb +9 -0
- data/spec/unit/modifiable_spec.rb +84 -0
- data/spec/unit/ole_wrapper_spec.rb +337 -0
- data/spec/unit/qb_collection_spec.rb +13 -0
- data/spec/unit/qbfc_const_spec.rb +10 -0
- data/spec/unit/qbfc_spec.rb +10 -0
- data/spec/unit/report_spec.rb +12 -0
- data/spec/unit/request_query_survey.txt +48 -0
- data/spec/unit/request_spec.rb +486 -0
- data/spec/unit/session_spec.rb +144 -0
- data/spec/unit/terms/generated_spec.rb +14 -0
- data/spec/unit/terms_spec.rb +18 -0
- data/spec/unit/transaction_finders_spec.rb +125 -0
- data/spec/unit/transaction_spec.rb +94 -0
- data/spec/unit/transactions/generated_spec.rb +20 -0
- data/spec/unit/voidable_spec.rb +32 -0
- data/tasks/qbfc_tasks.rake +4 -0
- data/uninstall.rb +1 -0
- metadata +180 -0
data/lib/qbfc/session.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
module QBFC
|
2
|
+
class QuickbooksClosedError < RuntimeError #:nodoc:
|
3
|
+
end
|
4
|
+
class SetValueMissing < RuntimeError#:nodoc:
|
5
|
+
end
|
6
|
+
class QBXMLVersionError < RuntimeError#:nodoc:
|
7
|
+
end
|
8
|
+
class UnknownRequestError < RuntimeError#:nodoc:
|
9
|
+
end
|
10
|
+
class BaseClassNewError < RuntimeError#:nodoc:
|
11
|
+
end
|
12
|
+
class NotSavableError < RuntimeError#:nodoc:
|
13
|
+
end
|
14
|
+
|
15
|
+
# Encapsulates a QBFC session.
|
16
|
+
#
|
17
|
+
# QBFC::Session.open(:app_name => 'Test Application') do |qb|
|
18
|
+
# qb.customers.find(:all).each do |customer|
|
19
|
+
# puts customer.full_name
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# qb = QBFC::Session.new(:app_name => 'Test Application')
|
24
|
+
# qb.customers.find(:all).each do |customer|
|
25
|
+
# puts customer.full_name
|
26
|
+
# end
|
27
|
+
# qb.close
|
28
|
+
#
|
29
|
+
# A QBFC::Session abstracts the ole_methods so that more conventional Ruby method names are used,
|
30
|
+
# e.g. <tt>full_name</tt> instead of <tt>FullName.GetValue()</tt>.
|
31
|
+
#
|
32
|
+
# This also allows a shortcut for setting up Quickbooks objects:
|
33
|
+
#
|
34
|
+
# - session.customers.find(:all) instead of QBFC::Customer.find(session, :all)
|
35
|
+
# - session.customer('CustomerFullName') instead of QBFC::Customer.find(session, 'CustomerFullName')
|
36
|
+
# - session.customer instead of QBFC::Customer.find(session, :first)
|
37
|
+
class Session
|
38
|
+
class << self
|
39
|
+
|
40
|
+
# Open a QBFC session. Takes options as a hash, and an optional block. Options are:
|
41
|
+
#
|
42
|
+
# - +app_name+: Name that the application sends to Quickbooks (used for allowing/denying access)
|
43
|
+
# (defaults to 'Ruby QBFC Application'
|
44
|
+
# - +app_id+: Per the Quickbooks SDK (QBFC Language Reference):
|
45
|
+
# 'Normally not assigned. Use an empty string for appID.' An empty string is passed by default.
|
46
|
+
# - +conn_type+: QBFC_CONST::CtUnknown, CtLocalQBD, CtRemoteQBD, CtLocalQBDLaunchUI, or CtRemoteQBOE.
|
47
|
+
# Default is QBFC_CONST::CtLocalQBD (1)
|
48
|
+
# - +filename+: The full path to the Quickbooks file; leave blank to connect to the currently
|
49
|
+
# open company file. Default is an empty string (Quickbooks should be running).
|
50
|
+
# - +open_mode+: The desired access mode. It can be one of three values:
|
51
|
+
# - QBFC_CONST::OmSingleUser (specifies single-user mode)
|
52
|
+
# - QBFC_CONST::OmMultiUser (specifies multi-user mode)
|
53
|
+
# - QBFC_CONST::OmDontCare (accept whatever mode is currently in effect, or single-user mode if no other mode is in effect)
|
54
|
+
# Default is QBFC_CONST::OmDontCare
|
55
|
+
#
|
56
|
+
# If given a block, it yields the Session object and closes the Session and Connection
|
57
|
+
# when the block closes.
|
58
|
+
#
|
59
|
+
# Otherwise, it returns the new Session object.
|
60
|
+
|
61
|
+
def open(*options, &block)
|
62
|
+
qb = new(*options)
|
63
|
+
if block_given?
|
64
|
+
begin
|
65
|
+
yield qb
|
66
|
+
ensure
|
67
|
+
qb.close
|
68
|
+
end
|
69
|
+
else
|
70
|
+
qb
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
# See Session.open for initialization options.
|
77
|
+
def initialize(options = {})
|
78
|
+
ole_object = WIN32OLE.new("QBFC6.QBSessionManager")
|
79
|
+
|
80
|
+
ole_object.OpenConnection2(options[:app_id].to_s,
|
81
|
+
(options[:app_name] || "Ruby QBFC Application"),
|
82
|
+
(options[:conn_type] || QBFC_CONST::CtLocalQBD))
|
83
|
+
|
84
|
+
begin
|
85
|
+
ole_object.BeginSession(options[:filename].to_s,
|
86
|
+
(options[:open_mode] || QBFC_CONST::OmDontCare))
|
87
|
+
rescue WIN32OLERuntimeError
|
88
|
+
ole_object.CloseConnection
|
89
|
+
ole_object = nil
|
90
|
+
raise(QBFC::QuickbooksClosedError,
|
91
|
+
"BeginSession failed: Quickbooks must be open or a valid filename specified.\n\n#{$!}")
|
92
|
+
end
|
93
|
+
|
94
|
+
@ole_object = QBFC::OLEWrapper.new(ole_object)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Close the session with Quickbooks. If this is ommitted, Quickbooks will not close.
|
98
|
+
# Using a block with Session.open ensures the session is closed.
|
99
|
+
def close
|
100
|
+
@ole_object.EndSession
|
101
|
+
@ole_object.CloseConnection
|
102
|
+
@ole_object = nil
|
103
|
+
end
|
104
|
+
|
105
|
+
# Generate a report with the given +name+ and +args+.
|
106
|
+
# (See QBFC::Report.get for details).
|
107
|
+
def report(name, *args)
|
108
|
+
Report.get(self, name, *args)
|
109
|
+
end
|
110
|
+
|
111
|
+
# The classes method allows using <tt>session.classes.find</tt> instead of
|
112
|
+
# <tt>session.q_b_classes.find</tt>, for finds on QBClass.
|
113
|
+
def classes
|
114
|
+
QBFC::QBCollection.new(self, :QBClass)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Responsible for the conversion of ole_method name to more convential Ruby method names.
|
118
|
+
# This specifies the methods for setting up an Entity, such as a Customer, directly, which is
|
119
|
+
# not included in OLEWrapper (setting up entities that are children of another entity is).
|
120
|
+
# Send other missing methods on to OLE Wrapper
|
121
|
+
def method_missing(symbol, *params) #:nodoc:
|
122
|
+
if (('a'..'z') === symbol.to_s[0].chr && symbol.to_s[-1].chr != '=')
|
123
|
+
camelized_method = symbol.to_s.camelize.to_sym
|
124
|
+
if camelized_method.to_s =~ /Terms\Z/
|
125
|
+
single_camelized_method = camelized_method
|
126
|
+
else
|
127
|
+
single_camelized_method = symbol.to_s.singularize.camelize.to_sym
|
128
|
+
end
|
129
|
+
if QBFC.const_defined?(camelized_method) && camelized_method.to_s !~ /Terms\Z/
|
130
|
+
if params[0]
|
131
|
+
return QBFC::const_get(camelized_method).find_by_unique_id(self, params[0])
|
132
|
+
else
|
133
|
+
return QBFC::const_get(camelized_method).find(self, :first)
|
134
|
+
end
|
135
|
+
elsif QBFC.const_defined?(single_camelized_method)
|
136
|
+
return QBFC::QBCollection.new(self, single_camelized_method)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Don't want to pass an OLEWrapper to a WIN32OLE method.
|
141
|
+
params = params.collect{ |p| p.respond_to?(:ole_object) ? p.ole_object : p }
|
142
|
+
|
143
|
+
@ole_object.qbfc_method_missing(self, symbol, *params)
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
data/lib/qbfc/terms.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'qbfc/voidable'
|
2
|
+
|
3
|
+
module QBFC
|
4
|
+
class Transaction < Element
|
5
|
+
is_base_class
|
6
|
+
ID_NAME = "TxnID"
|
7
|
+
|
8
|
+
class << self
|
9
|
+
|
10
|
+
# Find by Reference Number of the Transaction record.
|
11
|
+
# +options+ are the same as those for in +find+.
|
12
|
+
def find_by_ref(sess, ref, options = {})
|
13
|
+
q = create_query(sess)
|
14
|
+
q.query.RefNumberList.Add(ref)
|
15
|
+
find(sess, :first, q, options)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Find by TxnID of List record.
|
19
|
+
# +options+ are the same as those for in +find+.
|
20
|
+
def find_by_id(sess, id, options = {})
|
21
|
+
q = create_query(sess)
|
22
|
+
q.query.TxnIDList.Add(id)
|
23
|
+
find(sess, :first, q, options)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Find by either ref or id. Tries id first, then ref.
|
27
|
+
def find_by_ref_or_id(*args)
|
28
|
+
find_by_id(*args) || find_by_ref(*args)
|
29
|
+
end
|
30
|
+
|
31
|
+
alias_method :find_by_unique_id, :find_by_ref_or_id
|
32
|
+
|
33
|
+
def base_class_find(sess, what, q, options)
|
34
|
+
q.IncludeRetElementList.Add(self::ID_NAME)
|
35
|
+
q.IncludeRetElementList.Add('TxnType')
|
36
|
+
list = q.response
|
37
|
+
|
38
|
+
if list.nil?
|
39
|
+
(what == :all) ? [] : nil
|
40
|
+
else
|
41
|
+
ary = (0..(list.Count - 1)).collect { |i|
|
42
|
+
element = list.GetAt(i)
|
43
|
+
ret_class_name = element.TxnType.GetAsString
|
44
|
+
if QBFC::const_defined?(ret_class_name)
|
45
|
+
ret_class = QBFC::const_get(ret_class_name)
|
46
|
+
ret_class.find(sess, element.send(ret_class::ID_NAME).GetValue, options.dup)
|
47
|
+
else
|
48
|
+
find(sess, element.send(Transaction::ID_NAME).GetValue, options.dup.merge(:ignore_base_class => true))
|
49
|
+
end
|
50
|
+
}
|
51
|
+
|
52
|
+
if what == :all
|
53
|
+
ary
|
54
|
+
else
|
55
|
+
ary[0]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
# Alias of TxnID for this record.
|
63
|
+
def id
|
64
|
+
@ole.txn_id
|
65
|
+
end
|
66
|
+
|
67
|
+
# Delete this Transaction
|
68
|
+
def delete
|
69
|
+
req = QBFC::Request.new(@sess, "TxnDel")
|
70
|
+
req.txn_del_type = QBFC_CONST::const_get("Tdt#{qb_name}")
|
71
|
+
req.txn_id = id
|
72
|
+
req.submit
|
73
|
+
return true
|
74
|
+
end
|
75
|
+
|
76
|
+
# Change cleared status of transaction
|
77
|
+
# status can be one of:
|
78
|
+
# - QBFC::CsCleared (or true)
|
79
|
+
# - QBFC::CsNotCleared (or false)
|
80
|
+
# - QBFC::CsPending
|
81
|
+
def cleared_status=(status)
|
82
|
+
req = QBFC::Request.new(@sess, "ClearedStatusMod")
|
83
|
+
req.txn_id = id
|
84
|
+
status = QBFC_CONST::CsCleared if status === true
|
85
|
+
status = QBFC_CONST::CsNotCleared if status === false
|
86
|
+
req.cleared_status = status
|
87
|
+
req.submit
|
88
|
+
return status
|
89
|
+
end
|
90
|
+
|
91
|
+
# Display the Transaction add (for new records) or edit dialog box
|
92
|
+
def display
|
93
|
+
if new_record?
|
94
|
+
req = QBFC::Request.new(@sess, "TxnDisplayAdd")
|
95
|
+
req.txn_display_add_type = QBFC_CONST::const_get("Tdat#{qb_name}")
|
96
|
+
else
|
97
|
+
req = QBFC::Request.new(@sess, "TxnDisplayMod")
|
98
|
+
req.txn_display_mod_type = QBFC_CONST::const_get("Tdmt#{qb_name}")
|
99
|
+
req.txn_id = id
|
100
|
+
end
|
101
|
+
req.submit
|
102
|
+
return true
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Require subclass files
|
108
|
+
Dir.new(File.dirname(__FILE__) + '/transactions').each do |file|
|
109
|
+
require('qbfc/transactions/' + File.basename(file)) if File.extname(file) == ".rb"
|
110
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module QBFC
|
2
|
+
|
3
|
+
# Generated Transaction types
|
4
|
+
TXN_TYPES = %w{ARRefundCreditCard Bill BillPaymentCheck BillPaymentCreditCard
|
5
|
+
BuildAssembly Charge Check CreditCardCharge CreditCardCredit CreditMemo
|
6
|
+
Deposit Estimate InventoryAdjustment Invoice ItemReceipt JournalEntry
|
7
|
+
PurchaseOrder ReceivePayment SalesOrder SalesReceipt SalesTaxPaymentCheck
|
8
|
+
TimeTracking VehicleMileage VendorCredit}
|
9
|
+
|
10
|
+
# Generated Transaction types that support TxnVoid Request
|
11
|
+
TXN_VOIDABLE_TYPES = %w{ARRefundCreditCard Bill BillPaymentCheck
|
12
|
+
BillPaymentCreditCard Charge Check CreditCardCharge CreditCardCredit
|
13
|
+
CreditMemo Deposit InventoryAdjustment Invoice ItemReceipt JournalEntry
|
14
|
+
SalesReceipt VendorCredit}
|
15
|
+
|
16
|
+
# Generated Transaction types that don't support Mod Requests
|
17
|
+
TXN_NO_MOD_TYPES = %w{ARRefundCreditCard BillPaymentCreditCard Deposit
|
18
|
+
InventoryAdjustment VehicleMileage VendorCredit }
|
19
|
+
|
20
|
+
# Generate Transaction subclasses
|
21
|
+
generate(TXN_TYPES, Transaction,
|
22
|
+
{ Modifiable => (TXN_TYPES - TXN_NO_MOD_TYPES),
|
23
|
+
Voidable => TXN_VOIDABLE_TYPES })
|
24
|
+
|
25
|
+
end
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "QBFC::Customer.new" do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@integration = QBFC::Integration.new
|
7
|
+
@sess = @integration.session
|
8
|
+
end
|
9
|
+
|
10
|
+
after(:each) do
|
11
|
+
@integration.close
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should create a new customer" do
|
15
|
+
old_count = @sess.customers.find(:all).length
|
16
|
+
|
17
|
+
c = @sess.customers.new
|
18
|
+
c.name = "Cranky Customer"
|
19
|
+
c.is_active = true
|
20
|
+
c.last_name = "McCustomer"
|
21
|
+
c.save
|
22
|
+
|
23
|
+
n = @sess.customers.find("Cranky Customer")
|
24
|
+
n.name.should == "Cranky Customer"
|
25
|
+
n.is_active.should be(true)
|
26
|
+
n.last_name.should == "McCustomer"
|
27
|
+
|
28
|
+
@sess.customers.find(:all).length.should == old_count + 1
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe QBFC::Base do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@integration = QBFC::Integration::reader
|
7
|
+
@sess = @integration.session
|
8
|
+
end
|
9
|
+
|
10
|
+
after(:each) do
|
11
|
+
@integration.close
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should do nothing" do
|
15
|
+
true.should be_true
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# This spec describes "belong to" style relationships,
|
4
|
+
# that is where QuickBooks specifies a Ref to another object.
|
5
|
+
|
6
|
+
describe "belongs to: " do
|
7
|
+
|
8
|
+
before(:each) do
|
9
|
+
@integration = QBFC::Integration::reader
|
10
|
+
@sess = @integration.session
|
11
|
+
end
|
12
|
+
|
13
|
+
after(:each) do
|
14
|
+
@integration.close
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "Bob Customer" do
|
18
|
+
before(:each) do
|
19
|
+
@customer = @sess.customers.find("Bob Customer")
|
20
|
+
end
|
21
|
+
|
22
|
+
it "has terms" do
|
23
|
+
@customer.terms.should be_kind_of(QBFC::Terms)
|
24
|
+
@customer.terms.id.should == @sess.terms.find("Net 30").id
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should not have a sales rep" do
|
28
|
+
@customer.sales_rep.should be_nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "Check to ABC Supplies" do
|
33
|
+
before(:each) do
|
34
|
+
@check = @sess.checks.find_by_ref("1000")
|
35
|
+
end
|
36
|
+
|
37
|
+
it "has a payee" do
|
38
|
+
@check.payee.should be_kind_of(QBFC::Vendor)
|
39
|
+
@check.payee.id.should == @sess.vendors.find("ABC Supplies").id
|
40
|
+
end
|
41
|
+
|
42
|
+
it "has an account" do
|
43
|
+
@check.account.should be_kind_of(QBFC::Account)
|
44
|
+
@check.account.id.should == @sess.accounts.find("Checking").id
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "Invoice to Customer Bob" do
|
49
|
+
before(:each) do
|
50
|
+
@invoice = @sess.invoices.find_by_ref("1")
|
51
|
+
end
|
52
|
+
|
53
|
+
it "has a template" do
|
54
|
+
@invoice.template.should be_kind_of(QBFC::Template)
|
55
|
+
@invoice.template.id.should == @sess.templates.find("Intuit Service Invoice").id
|
56
|
+
end
|
57
|
+
|
58
|
+
it "has a Customer" do
|
59
|
+
@invoice.customer.should be_kind_of(QBFC::Customer)
|
60
|
+
@invoice.customer.id.should == @sess.customers.find("Bob Customer").id
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|