Skip to content

Commit 1f2bdb2

Browse files
authored
Merge pull request #328 from ej2/0.9.5
0.9.5
2 parents 5d29d1f + b12f7cf commit 1f2bdb2

18 files changed

+500
-296
lines changed

CHANGELOG.rst

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
Changelog
22
=========
3+
* 0.9.5 (November 1, 2023)
4+
* Added the ability to void all voidable QB types
5+
* Added to_ref to CreditMemo object
6+
* Added ProjectRef and ShipFromAddr to Estimate
7+
* Added missing initialization for objects on DiscountLineDetail, Estimate, Employee, and Invoice
8+
39
* 0.9.4 (August 29, 2023)
410
* Removed python 2 compatible decorators
511
* Removed python 2 dependencies

Pipfile

-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,3 @@ simplejson = ">=3.19.1"
1515
nose = "*"
1616
coverage = "*"
1717
twine = "*"
18-
19-
[requires]
20-
python_version = "3.8"

Pipfile.lock

+282-251
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

quickbooks/mixins.py

+51-6
Original file line numberDiff line numberDiff line change
@@ -119,21 +119,66 @@ def send(self, qb=None, send_to=None):
119119

120120

121121
class VoidMixin(object):
122+
123+
def get_void_params(self):
124+
qb_object_params_map = {
125+
"Payment": {
126+
"operation": "update",
127+
"include": "void"
128+
},
129+
"SalesReceipt": {
130+
"operation": "update",
131+
"include": "void"
132+
},
133+
"BillPayment": {
134+
"operation": "update",
135+
"include": "void"
136+
},
137+
"Invoice": {
138+
"operation": "void",
139+
},
140+
}
141+
# setting the default operation to void (the original behavior)
142+
return qb_object_params_map.get(self.qbo_object_name, {"operation": "void"})
143+
144+
def get_void_data(self):
145+
qb_object_params_map = {
146+
"Payment": {
147+
"Id": self.Id,
148+
"SyncToken": self.SyncToken,
149+
"sparse": True
150+
},
151+
"SalesReceipt": {
152+
"Id": self.Id,
153+
"SyncToken": self.SyncToken,
154+
"sparse": True
155+
},
156+
"BillPayment": {
157+
"Id": self.Id,
158+
"SyncToken": self.SyncToken,
159+
"sparse": True
160+
},
161+
"Invoice": {
162+
"Id": self.Id,
163+
"SyncToken": self.SyncToken,
164+
},
165+
}
166+
# setting the default operation to void (the original behavior)
167+
return qb_object_params_map.get(self.qbo_object_name, {"operation": "void"})
168+
122169
def void(self, qb=None):
123170
if not qb:
124171
qb = QuickBooks()
125172

126173
if not self.Id:
127174
raise QuickbooksException('Cannot void unsaved object')
128175

129-
data = {
130-
'Id': self.Id,
131-
'SyncToken': self.SyncToken,
132-
}
133-
134176
endpoint = self.qbo_object_name.lower()
135177
url = "{0}/company/{1}/{2}".format(qb.api_url, qb.company_id, endpoint)
136-
results = qb.post(url, json.dumps(data), params={'operation': 'void'})
178+
179+
data = self.get_void_data()
180+
params = self.get_void_params()
181+
results = qb.post(url, json.dumps(data), params=params)
137182

138183
return results
139184

quickbooks/objects/attachable.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def save(self, qb=None):
5858
else:
5959
json_data = qb.create_object(self.qbo_object_name, self.to_json(), _file_path=self._FilePath)
6060

61-
if self.FileName:
61+
if self.Id is None and self.FileName:
6262
obj = type(self).from_json(json_data['AttachableResponse'][0]['Attachable'])
6363
else:
6464
obj = type(self).from_json(json_data['Attachable'])

quickbooks/objects/billpayment.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from .base import QuickbooksBaseObject, Ref, LinkedTxn, QuickbooksManagedObject, LinkedTxnMixin, \
22
QuickbooksTransactionEntity
3-
from ..mixins import DeleteMixin
3+
from ..mixins import DeleteMixin, VoidMixin
44

55

66
class CheckPayment(QuickbooksBaseObject):
@@ -47,7 +47,7 @@ def __str__(self):
4747
return str(self.Amount)
4848

4949

50-
class BillPayment(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin):
50+
class BillPayment(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin, VoidMixin):
5151
"""
5252
QBO definition: A BillPayment entity represents the financial transaction of payment
5353
of bills that the business owner receives from a vendor for goods or services purchased

quickbooks/objects/creditmemo.py

+8
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,11 @@ def __init__(self):
7070

7171
def __str__(self):
7272
return str(self.TotalAmt)
73+
74+
def to_ref(self):
75+
ref = Ref()
76+
77+
ref.type = self.qbo_object_name
78+
ref.value = self.Id
79+
80+
return ref

quickbooks/objects/detailline.py

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __init__(self):
5151
self.Discount = None
5252
self.ClassRef = None
5353
self.TaxCodeRef = None
54+
self.DiscountAccountRef = None
5455
self.PercentBased = False
5556
self.DiscountPercent = 0
5657

quickbooks/objects/employee.py

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def __init__(self):
3535
self.BillableTime = False
3636

3737
self.PrimaryAddr = None
38+
self.PrimaryPhone = None
3839

3940
def __str__(self):
4041
return self.DisplayName

quickbooks/objects/estimate.py

+5
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ class Estimate(DeleteMixin,
1919
class_dict = {
2020
"BillAddr": Address,
2121
"ShipAddr": Address,
22+
"ShipFromAddr": Address,
2223
"CustomerRef": Ref,
24+
"ProjectRef": Ref,
2325
"TxnTaxDetail": TxnTaxDetail,
2426
"CustomerMemo": CustomerMemo,
2527
"BillEmail": EmailAddress,
@@ -64,9 +66,12 @@ def __init__(self):
6466
self.AcceptedDate = None
6567
self.GlobalTaxCalculation = "TaxExcluded"
6668
self.BillAddr = None
69+
self.DepartmentRef = None
6770
self.ShipAddr = None
71+
self.ShipFromAddr = None
6872
self.BillEmail = None
6973
self.CustomerRef = None
74+
self.ProjectRef = None
7075
self.TxnTaxDetail = None
7176
self.CustomerMemo = None
7277
self.ClassRef = None

quickbooks/objects/invoice.py

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ def __init__(self):
9595
self.TxnTaxDetail = None
9696
self.DeliveryInfo = None
9797
self.RecurDataRef = None
98+
self.SalesTermRef = None
99+
self.ShipMethodRef = None
98100
self.TaxExemptionRef = None
99101
self.MetaData = None
100102

quickbooks/objects/payment.py

+2-21
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
LinkedTxnMixin, MetaData
44
from ..client import QuickBooks
55
from .creditcardpayment import CreditCardPayment
6-
from ..mixins import DeleteMixin
6+
from ..mixins import DeleteMixin, VoidMixin
77
import json
88

99

@@ -21,7 +21,7 @@ def __str__(self):
2121
return str(self.Amount)
2222

2323

24-
class Payment(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin):
24+
class Payment(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin, VoidMixin):
2525
"""
2626
QBO definition: A Payment entity records a payment in QuickBooks. The payment can be
2727
applied for a particular customer against multiple Invoices and Credit Memos. It can also
@@ -81,24 +81,5 @@ def __init__(self):
8181
# These fields are for minor version 4
8282
self.TransactionLocationType = None
8383

84-
def void(self, qb=None):
85-
if not qb:
86-
qb = QuickBooks()
87-
88-
if not self.Id:
89-
raise qb.QuickbooksException('Cannot void unsaved object')
90-
91-
data = {
92-
'Id': self.Id,
93-
'SyncToken': self.SyncToken,
94-
'sparse': True
95-
}
96-
97-
endpoint = self.qbo_object_name.lower()
98-
url = "{0}/company/{1}/{2}".format(qb.api_url, qb.company_id, endpoint)
99-
results = qb.post(url, json.dumps(data), params={'operation': 'update', 'include': 'void'})
100-
101-
return results
102-
10384
def __str__(self):
10485
return str(self.TotalAmt)

quickbooks/objects/salesreceipt.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
EmailAddress, QuickbooksTransactionEntity, LinkedTxn
33
from .tax import TxnTaxDetail
44
from .detailline import DetailLine
5-
from ..mixins import QuickbooksPdfDownloadable, DeleteMixin
5+
from ..mixins import QuickbooksPdfDownloadable, DeleteMixin, VoidMixin
66

77

88
class SalesReceipt(DeleteMixin, QuickbooksPdfDownloadable, QuickbooksManagedObject,
9-
QuickbooksTransactionEntity, LinkedTxnMixin):
9+
QuickbooksTransactionEntity, LinkedTxnMixin, VoidMixin):
1010
"""
1111
QBO definition: SalesReceipt represents the sales receipt that is given to a customer.
1212
A sales receipt is similar to an invoice. However, for a sales receipt, payment is received

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def read(*parts):
1010
return fp.read()
1111

1212

13-
VERSION = (0, 9, 4)
13+
VERSION = (0, 9, 5)
1414
version = '.'.join(map(str, VERSION))
1515

1616
setup(

tests/integration/test_billpayment.py

+43-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from datetime import datetime
22

3+
from quickbooks.objects import AccountBasedExpenseLine, Ref, AccountBasedExpenseLineDetail
34
from quickbooks.objects.account import Account
45
from quickbooks.objects.bill import Bill
56
from quickbooks.objects.billpayment import BillPayment, BillPaymentLine, CheckPayment
@@ -14,12 +15,30 @@ def setUp(self):
1415
self.account_number = datetime.now().strftime('%d%H%M')
1516
self.name = "Test Account {0}".format(self.account_number)
1617

17-
def test_create(self):
18+
def create_bill(self, amount):
19+
bill = Bill()
20+
line = AccountBasedExpenseLine()
21+
line.Amount = amount
22+
line.DetailType = "AccountBasedExpenseLineDetail"
23+
24+
account_ref = Ref()
25+
account_ref.type = "Account"
26+
account_ref.value = 1
27+
line.AccountBasedExpenseLineDetail = AccountBasedExpenseLineDetail()
28+
line.AccountBasedExpenseLineDetail.AccountRef = account_ref
29+
bill.Line.append(line)
30+
31+
vendor = Vendor.all(max_results=1, qb=self.qb_client)[0]
32+
bill.VendorRef = vendor.to_ref()
33+
34+
return bill.save(qb=self.qb_client)
35+
36+
def create_bill_payment(self, bill, amount, private_note, pay_type):
1837
bill_payment = BillPayment()
1938

20-
bill_payment.PayType = "Check"
21-
bill_payment.TotalAmt = 200
22-
bill_payment.PrivateNote = "Private Note"
39+
bill_payment.PayType = pay_type
40+
bill_payment.TotalAmt = amount
41+
bill_payment.PrivateNote = private_note
2342

2443
vendor = Vendor.all(max_results=1, qb=self.qb_client)[0]
2544
bill_payment.VendorRef = vendor.to_ref()
@@ -31,14 +50,18 @@ def test_create(self):
3150
ap_account = Account.where("AccountSubType = 'AccountsPayable'", qb=self.qb_client)[0]
3251
bill_payment.APAccountRef = ap_account.to_ref()
3352

34-
bill = Bill.all(max_results=1, qb=self.qb_client)[0]
35-
3653
line = BillPaymentLine()
3754
line.LinkedTxn.append(bill.to_linked_txn())
3855
line.Amount = 200
3956

4057
bill_payment.Line.append(line)
41-
bill_payment.save(qb=self.qb_client)
58+
return bill_payment.save(qb=self.qb_client)
59+
60+
def test_create(self):
61+
# create new bill for testing, reusing the same bill will cause Line to be empty
62+
# and the new bill payment will be voided automatically
63+
bill = self.create_bill(amount=200)
64+
bill_payment = self.create_bill_payment(bill, 200, "Private Note", "Check")
4265

4366
query_bill_payment = BillPayment.get(bill_payment.Id, qb=self.qb_client)
4467

@@ -48,3 +71,16 @@ def test_create(self):
4871

4972
self.assertEqual(len(query_bill_payment.Line), 1)
5073
self.assertEqual(query_bill_payment.Line[0].Amount, 200.0)
74+
75+
def test_void(self):
76+
bill = self.create_bill(amount=200)
77+
bill_payment = self.create_bill_payment(bill, 200, "Private Note", "Check")
78+
query_payment = BillPayment.get(bill_payment.Id, qb=self.qb_client)
79+
self.assertEqual(query_payment.TotalAmt, 200.0)
80+
self.assertNotIn('Voided', query_payment.PrivateNote)
81+
82+
bill_payment.void(qb=self.qb_client)
83+
query_payment = BillPayment.get(bill_payment.Id, qb=self.qb_client)
84+
85+
self.assertEqual(query_payment.TotalAmt, 0.0)
86+
self.assertIn('Voided', query_payment.PrivateNote)

tests/integration/test_invoice.py

+11
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,14 @@ def test_delete(self):
7575

7676
query_invoice = Invoice.filter(Id=invoice_id, qb=self.qb_client)
7777
self.assertEqual([], query_invoice)
78+
79+
def test_void(self):
80+
customer = Customer.all(max_results=1, qb=self.qb_client)[0]
81+
invoice = self.create_invoice(customer)
82+
invoice_id = invoice.Id
83+
invoice.void(qb=self.qb_client)
84+
85+
query_invoice = Invoice.get(invoice_id, qb=self.qb_client)
86+
self.assertEqual(query_invoice.Balance, 0.0)
87+
self.assertEqual(query_invoice.TotalAmt, 0.0)
88+
self.assertIn('Voided', query_invoice.PrivateNote)

0 commit comments

Comments
 (0)