Skip to content

Commit

Permalink
Merge pull request #328 from ej2/0.9.5
Browse files Browse the repository at this point in the history
0.9.5
  • Loading branch information
ej2 committed Nov 1, 2023
2 parents 5d29d1f + b12f7cf commit 1f2bdb2
Show file tree
Hide file tree
Showing 18 changed files with 500 additions and 296 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
Changelog
=========
* 0.9.5 (November 1, 2023)
* Added the ability to void all voidable QB types
* Added to_ref to CreditMemo object
* Added ProjectRef and ShipFromAddr to Estimate
* Added missing initialization for objects on DiscountLineDetail, Estimate, Employee, and Invoice

* 0.9.4 (August 29, 2023)
* Removed python 2 compatible decorators
* Removed python 2 dependencies
Expand Down
3 changes: 0 additions & 3 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,3 @@ simplejson = ">=3.19.1"
nose = "*"
coverage = "*"
twine = "*"

[requires]
python_version = "3.8"
533 changes: 282 additions & 251 deletions Pipfile.lock

Large diffs are not rendered by default.

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
8 changes: 8 additions & 0 deletions quickbooks/objects/creditmemo.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,11 @@ def __init__(self):

def __str__(self):
return str(self.TotalAmt)

def to_ref(self):
ref = Ref()

ref.type = self.qbo_object_name
ref.value = self.Id

return ref
1 change: 1 addition & 0 deletions quickbooks/objects/detailline.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def __init__(self):
self.Discount = None
self.ClassRef = None
self.TaxCodeRef = None
self.DiscountAccountRef = None
self.PercentBased = False
self.DiscountPercent = 0

Expand Down
1 change: 1 addition & 0 deletions quickbooks/objects/employee.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def __init__(self):
self.BillableTime = False

self.PrimaryAddr = None
self.PrimaryPhone = None

def __str__(self):
return self.DisplayName
Expand Down
5 changes: 5 additions & 0 deletions quickbooks/objects/estimate.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ class Estimate(DeleteMixin,
class_dict = {
"BillAddr": Address,
"ShipAddr": Address,
"ShipFromAddr": Address,
"CustomerRef": Ref,
"ProjectRef": Ref,
"TxnTaxDetail": TxnTaxDetail,
"CustomerMemo": CustomerMemo,
"BillEmail": EmailAddress,
Expand Down Expand Up @@ -64,9 +66,12 @@ def __init__(self):
self.AcceptedDate = None
self.GlobalTaxCalculation = "TaxExcluded"
self.BillAddr = None
self.DepartmentRef = None
self.ShipAddr = None
self.ShipFromAddr = None
self.BillEmail = None
self.CustomerRef = None
self.ProjectRef = None
self.TxnTaxDetail = None
self.CustomerMemo = None
self.ClassRef = None
Expand Down
2 changes: 2 additions & 0 deletions quickbooks/objects/invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ def __init__(self):
self.TxnTaxDetail = None
self.DeliveryInfo = None
self.RecurDataRef = None
self.SalesTermRef = None
self.ShipMethodRef = None
self.TaxExemptionRef = None
self.MetaData = None

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
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def read(*parts):
return fp.read()


VERSION = (0, 9, 4)
VERSION = (0, 9, 5)
version = '.'.join(map(str, VERSION))

setup(
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)
Loading

0 comments on commit 1f2bdb2

Please sign in to comment.