Skip to content

Commit

Permalink
Merge pull request #322 from ZedObaia/qb-304-refactor-void-mixin
Browse files Browse the repository at this point in the history
Add the ability to void all voidable QB types fixes #304
  • Loading branch information
ej2 authored Nov 1, 2023
2 parents a02ca1b + 6abedee commit b40d4e3
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 41 deletions.
57 changes: 51 additions & 6 deletions quickbooks/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,21 +119,66 @@ def send(self, qb=None, send_to=None):


class VoidMixin(object):

def get_void_params(self):
qb_object_params_map = {
"Payment": {
"operation": "update",
"include": "void"
},
"SalesReceipt": {
"operation": "update",
"include": "void"
},
"BillPayment": {
"operation": "update",
"include": "void"
},
"Invoice": {
"operation": "void",
},
}
# setting the default operation to void (the original behavior)
return qb_object_params_map.get(self.qbo_object_name, {"operation": "void"})

def get_void_data(self):
qb_object_params_map = {
"Payment": {
"Id": self.Id,
"SyncToken": self.SyncToken,
"sparse": True
},
"SalesReceipt": {
"Id": self.Id,
"SyncToken": self.SyncToken,
"sparse": True
},
"BillPayment": {
"Id": self.Id,
"SyncToken": self.SyncToken,
"sparse": True
},
"Invoice": {
"Id": self.Id,
"SyncToken": self.SyncToken,
},
}
# setting the default operation to void (the original behavior)
return qb_object_params_map.get(self.qbo_object_name, {"operation": "void"})

def void(self, qb=None):
if not qb:
qb = QuickBooks()

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

data = {
'Id': self.Id,
'SyncToken': self.SyncToken,
}

endpoint = self.qbo_object_name.lower()
url = "{0}/company/{1}/{2}".format(qb.api_url, qb.company_id, endpoint)
results = qb.post(url, json.dumps(data), params={'operation': 'void'})

data = self.get_void_data()
params = self.get_void_params()
results = qb.post(url, json.dumps(data), params=params)

return results

Expand Down
2 changes: 1 addition & 1 deletion quickbooks/objects/attachable.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def save(self, qb=None):
else:
json_data = qb.create_object(self.qbo_object_name, self.to_json(), _file_path=self._FilePath)

if self.FileName:
if self.Id is None and self.FileName:
obj = type(self).from_json(json_data['AttachableResponse'][0]['Attachable'])
else:
obj = type(self).from_json(json_data['Attachable'])
Expand Down
4 changes: 2 additions & 2 deletions quickbooks/objects/billpayment.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .base import QuickbooksBaseObject, Ref, LinkedTxn, QuickbooksManagedObject, LinkedTxnMixin, \
QuickbooksTransactionEntity
from ..mixins import DeleteMixin
from ..mixins import DeleteMixin, VoidMixin


class CheckPayment(QuickbooksBaseObject):
Expand Down Expand Up @@ -47,7 +47,7 @@ def __str__(self):
return str(self.Amount)


class BillPayment(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin):
class BillPayment(DeleteMixin, QuickbooksManagedObject, QuickbooksTransactionEntity, LinkedTxnMixin, VoidMixin):
"""
QBO definition: A BillPayment entity represents the financial transaction of payment
of bills that the business owner receives from a vendor for goods or services purchased
Expand Down
23 changes: 2 additions & 21 deletions quickbooks/objects/payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
LinkedTxnMixin, MetaData
from ..client import QuickBooks
from .creditcardpayment import CreditCardPayment
from ..mixins import DeleteMixin
from ..mixins import DeleteMixin, VoidMixin
import json


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


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

def void(self, qb=None):
if not qb:
qb = QuickBooks()

if not self.Id:
raise qb.QuickbooksException('Cannot void unsaved object')

data = {
'Id': self.Id,
'SyncToken': self.SyncToken,
'sparse': True
}

endpoint = self.qbo_object_name.lower()
url = "{0}/company/{1}/{2}".format(qb.api_url, qb.company_id, endpoint)
results = qb.post(url, json.dumps(data), params={'operation': 'update', 'include': 'void'})

return results

def __str__(self):
return str(self.TotalAmt)
4 changes: 2 additions & 2 deletions quickbooks/objects/salesreceipt.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
EmailAddress, QuickbooksTransactionEntity, LinkedTxn
from .tax import TxnTaxDetail
from .detailline import DetailLine
from ..mixins import QuickbooksPdfDownloadable, DeleteMixin
from ..mixins import QuickbooksPdfDownloadable, DeleteMixin, VoidMixin


class SalesReceipt(DeleteMixin, QuickbooksPdfDownloadable, QuickbooksManagedObject,
QuickbooksTransactionEntity, LinkedTxnMixin):
QuickbooksTransactionEntity, LinkedTxnMixin, VoidMixin):
"""
QBO definition: SalesReceipt represents the sales receipt that is given to a customer.
A sales receipt is similar to an invoice. However, for a sales receipt, payment is received
Expand Down
50 changes: 43 additions & 7 deletions tests/integration/test_billpayment.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from datetime import datetime

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

def test_create(self):
def create_bill(self, amount):
bill = Bill()
line = AccountBasedExpenseLine()
line.Amount = amount
line.DetailType = "AccountBasedExpenseLineDetail"

account_ref = Ref()
account_ref.type = "Account"
account_ref.value = 1
line.AccountBasedExpenseLineDetail = AccountBasedExpenseLineDetail()
line.AccountBasedExpenseLineDetail.AccountRef = account_ref
bill.Line.append(line)

vendor = Vendor.all(max_results=1, qb=self.qb_client)[0]
bill.VendorRef = vendor.to_ref()

return bill.save(qb=self.qb_client)

def create_bill_payment(self, bill, amount, private_note, pay_type):
bill_payment = BillPayment()

bill_payment.PayType = "Check"
bill_payment.TotalAmt = 200
bill_payment.PrivateNote = "Private Note"
bill_payment.PayType = pay_type
bill_payment.TotalAmt = amount
bill_payment.PrivateNote = private_note

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

bill = Bill.all(max_results=1, qb=self.qb_client)[0]

line = BillPaymentLine()
line.LinkedTxn.append(bill.to_linked_txn())
line.Amount = 200

bill_payment.Line.append(line)
bill_payment.save(qb=self.qb_client)
return bill_payment.save(qb=self.qb_client)

def test_create(self):
# create new bill for testing, reusing the same bill will cause Line to be empty
# and the new bill payment will be voided automatically
bill = self.create_bill(amount=200)
bill_payment = self.create_bill_payment(bill, 200, "Private Note", "Check")

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

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

self.assertEqual(len(query_bill_payment.Line), 1)
self.assertEqual(query_bill_payment.Line[0].Amount, 200.0)

def test_void(self):
bill = self.create_bill(amount=200)
bill_payment = self.create_bill_payment(bill, 200, "Private Note", "Check")
query_payment = BillPayment.get(bill_payment.Id, qb=self.qb_client)
self.assertEqual(query_payment.TotalAmt, 200.0)
self.assertNotIn('Voided', query_payment.PrivateNote)

bill_payment.void(qb=self.qb_client)
query_payment = BillPayment.get(bill_payment.Id, qb=self.qb_client)

self.assertEqual(query_payment.TotalAmt, 0.0)
self.assertIn('Voided', query_payment.PrivateNote)
11 changes: 11 additions & 0 deletions tests/integration/test_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,14 @@ def test_delete(self):

query_invoice = Invoice.filter(Id=invoice_id, qb=self.qb_client)
self.assertEqual([], query_invoice)

def test_void(self):
customer = Customer.all(max_results=1, qb=self.qb_client)[0]
invoice = self.create_invoice(customer)
invoice_id = invoice.Id
invoice.void(qb=self.qb_client)

query_invoice = Invoice.get(invoice_id, qb=self.qb_client)
self.assertEqual(query_invoice.Balance, 0.0)
self.assertEqual(query_invoice.TotalAmt, 0.0)
self.assertIn('Voided', query_invoice.PrivateNote)
59 changes: 59 additions & 0 deletions tests/integration/test_salesreceipt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from datetime import datetime

from quickbooks.objects import SalesReceipt, Customer, \
SalesItemLine, SalesItemLineDetail, Item
from tests.integration.test_base import QuickbooksTestCase


class SalesReceiptTest(QuickbooksTestCase):
def setUp(self):
super(SalesReceiptTest, self).setUp()

self.account_number = datetime.now().strftime('%d%H%M')
self.name = "Test Account {0}".format(self.account_number)

def create_sales_receipt(self, qty=1, unit_price=100.0):
sales_receipt = SalesReceipt()
sales_receipt.TotalAmt = qty * unit_price
customer = Customer.all(max_results=1, qb=self.qb_client)[0]
sales_receipt.CustomerRef = customer.to_ref()
item = Item.all(max_results=1, qb=self.qb_client)[0]
line = SalesItemLine()
sales_item_line_detail = SalesItemLineDetail()
sales_item_line_detail.ItemRef = item.to_ref()
sales_item_line_detail.Qty = qty
sales_item_line_detail.UnitPrice = unit_price
today = datetime.now()
sales_item_line_detail.ServiceDate = today.strftime(
"%Y-%m-%d"
)
line.SalesItemLineDetail = sales_item_line_detail
line.Amount = qty * unit_price
sales_receipt.Line = [line]

return sales_receipt.save(qb=self.qb_client)

def test_create(self):
sales_receipt = self.create_sales_receipt(
qty=1,
unit_price=100.0
)
query_sales_receipt = SalesReceipt.get(sales_receipt.Id, qb=self.qb_client)

self.assertEqual(query_sales_receipt.TotalAmt, 100.0)
self.assertEqual(query_sales_receipt.Line[0].Amount, 100.0)
self.assertEqual(query_sales_receipt.Line[0].SalesItemLineDetail['Qty'], 1)
self.assertEqual(query_sales_receipt.Line[0].SalesItemLineDetail['UnitPrice'], 100.0)

def test_void(self):
sales_receipt = self.create_sales_receipt(
qty=1,
unit_price=100.0
)
query_sales_receipt = SalesReceipt.get(sales_receipt.Id, qb=self.qb_client)
self.assertEqual(query_sales_receipt.TotalAmt, 100.0)
self.assertNotIn('Voided', query_sales_receipt.PrivateNote)
sales_receipt.void(qb=self.qb_client)
query_sales_receipt = SalesReceipt.get(sales_receipt.Id, qb=self.qb_client)
self.assertEqual(query_sales_receipt.TotalAmt, 0.0)
self.assertIn('Voided', query_sales_receipt.PrivateNote)
25 changes: 23 additions & 2 deletions tests/unit/test_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import unittest
from urllib.parse import quote

from quickbooks.objects import Bill, Invoice
from quickbooks.objects import Bill, Invoice, Payment, BillPayment

from tests.integration.test_base import QuickbooksUnitTestCase

Expand Down Expand Up @@ -381,12 +381,33 @@ def test_send_with_send_to_email(self, mock_misc_op):

class VoidMixinTest(QuickbooksUnitTestCase):
@patch('quickbooks.mixins.QuickBooks.post')
def test_void(self, post):
def test_void_invoice(self, post):
invoice = Invoice()
invoice.Id = 2
invoice.void(qb=self.qb_client)
self.assertTrue(post.called)

@patch('quickbooks.mixins.QuickBooks.post')
def test_void_payment(self, post):
payment = Payment()
payment.Id = 2
payment.void(qb=self.qb_client)
self.assertTrue(post.called)

@patch('quickbooks.mixins.QuickBooks.post')
def test_void_sales_receipt(self, post):
sales_receipt = SalesReceipt()
sales_receipt.Id = 2
sales_receipt.void(qb=self.qb_client)
self.assertTrue(post.called)

@patch('quickbooks.mixins.QuickBooks.post')
def test_void_bill_payment(self, post):
bill_payment = BillPayment()
bill_payment.Id = 2
bill_payment.void(qb=self.qb_client)
self.assertTrue(post.called)

def test_delete_unsaved_exception(self):
from quickbooks.exceptions import QuickbooksException

Expand Down

0 comments on commit b40d4e3

Please sign in to comment.