Skip to content

Commit b40d4e3

Browse files
authored
Merge pull request #322 from ZedObaia/qb-304-refactor-void-mixin
Add the ability to void all voidable QB types fixes #304
2 parents a02ca1b + 6abedee commit b40d4e3

9 files changed

Lines changed: 194 additions & 41 deletions

File tree

quickbooks/mixins.py

Lines changed: 51 additions & 6 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 2 additions & 2 deletions
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/payment.py

Lines changed: 2 additions & 21 deletions
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

Lines changed: 2 additions & 2 deletions
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

tests/integration/test_billpayment.py

Lines changed: 43 additions & 7 deletions
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

Lines changed: 11 additions & 0 deletions
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)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from datetime import datetime
2+
3+
from quickbooks.objects import SalesReceipt, Customer, \
4+
SalesItemLine, SalesItemLineDetail, Item
5+
from tests.integration.test_base import QuickbooksTestCase
6+
7+
8+
class SalesReceiptTest(QuickbooksTestCase):
9+
def setUp(self):
10+
super(SalesReceiptTest, self).setUp()
11+
12+
self.account_number = datetime.now().strftime('%d%H%M')
13+
self.name = "Test Account {0}".format(self.account_number)
14+
15+
def create_sales_receipt(self, qty=1, unit_price=100.0):
16+
sales_receipt = SalesReceipt()
17+
sales_receipt.TotalAmt = qty * unit_price
18+
customer = Customer.all(max_results=1, qb=self.qb_client)[0]
19+
sales_receipt.CustomerRef = customer.to_ref()
20+
item = Item.all(max_results=1, qb=self.qb_client)[0]
21+
line = SalesItemLine()
22+
sales_item_line_detail = SalesItemLineDetail()
23+
sales_item_line_detail.ItemRef = item.to_ref()
24+
sales_item_line_detail.Qty = qty
25+
sales_item_line_detail.UnitPrice = unit_price
26+
today = datetime.now()
27+
sales_item_line_detail.ServiceDate = today.strftime(
28+
"%Y-%m-%d"
29+
)
30+
line.SalesItemLineDetail = sales_item_line_detail
31+
line.Amount = qty * unit_price
32+
sales_receipt.Line = [line]
33+
34+
return sales_receipt.save(qb=self.qb_client)
35+
36+
def test_create(self):
37+
sales_receipt = self.create_sales_receipt(
38+
qty=1,
39+
unit_price=100.0
40+
)
41+
query_sales_receipt = SalesReceipt.get(sales_receipt.Id, qb=self.qb_client)
42+
43+
self.assertEqual(query_sales_receipt.TotalAmt, 100.0)
44+
self.assertEqual(query_sales_receipt.Line[0].Amount, 100.0)
45+
self.assertEqual(query_sales_receipt.Line[0].SalesItemLineDetail['Qty'], 1)
46+
self.assertEqual(query_sales_receipt.Line[0].SalesItemLineDetail['UnitPrice'], 100.0)
47+
48+
def test_void(self):
49+
sales_receipt = self.create_sales_receipt(
50+
qty=1,
51+
unit_price=100.0
52+
)
53+
query_sales_receipt = SalesReceipt.get(sales_receipt.Id, qb=self.qb_client)
54+
self.assertEqual(query_sales_receipt.TotalAmt, 100.0)
55+
self.assertNotIn('Voided', query_sales_receipt.PrivateNote)
56+
sales_receipt.void(qb=self.qb_client)
57+
query_sales_receipt = SalesReceipt.get(sales_receipt.Id, qb=self.qb_client)
58+
self.assertEqual(query_sales_receipt.TotalAmt, 0.0)
59+
self.assertIn('Voided', query_sales_receipt.PrivateNote)

tests/unit/test_mixins.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import unittest
55
from urllib.parse import quote
66

7-
from quickbooks.objects import Bill, Invoice
7+
from quickbooks.objects import Bill, Invoice, Payment, BillPayment
88

99
from tests.integration.test_base import QuickbooksUnitTestCase
1010

@@ -381,12 +381,33 @@ def test_send_with_send_to_email(self, mock_misc_op):
381381

382382
class VoidMixinTest(QuickbooksUnitTestCase):
383383
@patch('quickbooks.mixins.QuickBooks.post')
384-
def test_void(self, post):
384+
def test_void_invoice(self, post):
385385
invoice = Invoice()
386386
invoice.Id = 2
387387
invoice.void(qb=self.qb_client)
388388
self.assertTrue(post.called)
389389

390+
@patch('quickbooks.mixins.QuickBooks.post')
391+
def test_void_payment(self, post):
392+
payment = Payment()
393+
payment.Id = 2
394+
payment.void(qb=self.qb_client)
395+
self.assertTrue(post.called)
396+
397+
@patch('quickbooks.mixins.QuickBooks.post')
398+
def test_void_sales_receipt(self, post):
399+
sales_receipt = SalesReceipt()
400+
sales_receipt.Id = 2
401+
sales_receipt.void(qb=self.qb_client)
402+
self.assertTrue(post.called)
403+
404+
@patch('quickbooks.mixins.QuickBooks.post')
405+
def test_void_bill_payment(self, post):
406+
bill_payment = BillPayment()
407+
bill_payment.Id = 2
408+
bill_payment.void(qb=self.qb_client)
409+
self.assertTrue(post.called)
410+
390411
def test_delete_unsaved_exception(self):
391412
from quickbooks.exceptions import QuickbooksException
392413

0 commit comments

Comments
 (0)