Skip to content

Commit

Permalink
Prepare first release.
Browse files Browse the repository at this point in the history
  • Loading branch information
bill-ash committed Dec 1, 2021
1 parent 8d5e952 commit 1377559
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 186 deletions.
7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[build-system]
requires = [
"setuptools>=42",
"wheel"
]

build-backend = "setuptools.build_meta"
2 changes: 1 addition & 1 deletion qbodbc/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
class QuickBooks:
pyodbc.pooling = False

def __init__(self, DSN=None, remote_dsn=None, ip="192.168.14.143", port=4500):
def __init__(self, DSN=None, remote_dsn=None, ip='', port=4500):
"""Create a local or remote connection"""
if DSN:
self.connection_string = ("DSN=" + DSN)
Expand Down
65 changes: 52 additions & 13 deletions qbodbc/objects/base_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,68 @@
# create, update, destroy mixin
# from qbodbc.client import Client

class BaseObject:
# Add something to remove None values
def to_dict(obj, classkey=None):
"""
Recursively converts Python object into a dictionary
Stolen from: https://github.com/ej2/python-quickbooks/blob/c82935bd07d9901d0e8d10765a7fcb06ad2ef53d/quickbooks/mixins.py#L89
"""
if isinstance(obj, dict):
data = {}
for (k, v) in obj.items():
data[k] = to_dict(v, classkey)
return data
elif hasattr(obj, "_ast"):
return to_dict(obj._ast())
elif hasattr(obj, "__iter__") and not isinstance(obj, str):
return [to_dict(v, classkey) for v in obj]
elif hasattr(obj, "__dict__"):
data = dict([(key, to_dict(value, classkey))
for key, value in obj.__dict__.items()
if not callable(value) and not key.startswith('_')])

if classkey is not None and hasattr(obj, "__class__"):
data[classkey] = obj.__class__.__name__
return data
else:
return obj

class ToDictMixin:
def to_dict(self):
return to_dict(self)


class BaseObject(ToDictMixin):

def to_names(self):
"""
Turns object attribute names into comma seperated string to be used
in insert statements.
"""
return ', '.join([key for key, value in self.__dict__.items() if value is not None])
return ', '.join([key for key, value in self.to_dict().items() if value is not None])

def to_values(self):
"""
Turns object attribute values into list of values to be used in
insert statements.
"""
return [value for key, value in self.__dict__.items() if value is not None]
return [value for key, value in self.to_dict().items() if value is not None]

def to_params(self):
"""
Creates a comma seperated string of question marks to be used
in paramterized queries. The number of question marks returns should
match the length of to_values().
"""
return ', '.join('?' * len([key for key, value in self.__dict__.items() if value is not None]))
return ', '.join('?' * len([key for key, value in self.to_dict().items() if value is not None]))

def update_obj(self, obj):
"""
Merge a child object with its parent or vica versa.
"""
for k, v in obj.to_dict().items():
if v:
if v is not None:
setattr(self, k, v)

def process_response():
# Something to handle errors in a uniform manner
Expand All @@ -34,19 +73,19 @@ def process_response():
def create(self):
q = (
f"insert into {self.__table_name__} "
f"({self.to_names()}) values"
f"({self.to_names()}) values "
f"({self.to_params()})"
)

return q

def update(self):
"""
update customer set
accountnumber = ?, customfield_xxx = ?
where listid = ?
"""
pass
# def update(self):
# """
# update customer set
# accountnumber = ?, customfield_xxx = ?
# where listid = ?
# """
# pass


def delete(self):
Expand Down
120 changes: 51 additions & 69 deletions qbodbc/objects/bill.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,124 +4,106 @@
from qbodbc.utils import Ref
from qbodbc.objects.base_object import BaseObject

class BillItemLine:
class BillItemLine(BaseObject):
"""
Add a line item to a bill. Minimum required fields. Other items:
- class: ItemLineClassRefFullName
- customer: ItemLineCustomerRefFullName
- customer:job: ItemLineCustomerRefFullName
- cogs account: ItemLineOverrideItemAccountRefFullName
"""
__table_name__ = 'BillItemLine'

def __init__(self, Item: str, Desc: str, Quantity: Decimal, Amount: Decimal, **kwargs):
self.ItemLineItemRefFullName = Item
self.ItemLineDesc = Desc
self.ItemLineQuantity = Quantity
self.ItemLineCost = Amount
self.FQSaveToCache = 1 # Default to save

self.ItemLineItemRefListId = kwargs.pop('ItemLineItemRefListId', None)
self.ItemLineCustomerRefFullName = kwargs.pop('ItemLineCustomerRefFullName', None)
self.ItemLineOverrideItemAccountRefFullName = kwargs.pop('ItemLineOverrideItemAccountRefFullName', None)
self.ItemLineClassRefFullName = kwargs.pop('ItemLineClassRefFullName', None)

class BillExpenseLine:
class BillExpenseLine(BaseObject):
__table_name__ = 'BillExpenseLine'

def __init__(self, account: str, memo: str, amount: Decimal, **kwargs):
self.ExpenseLineAccountRefFullName = account
self.ExpenseLineMemo = memo
self.ExpenseLineAmount = amount




class Bill(BaseObject):
__table__name__ = 'BillItemLine'
__table_name__ = 'Bill'

def __init__(self, **kwargs):
"""Bill with minimum field requirements."""

self.VendorRefFullName = kwargs.pop('Vendor', None)
self.APAccountRefFullName = kwargs.pop('APAccount', None)
self.TxnDate = kwargs.pop('TxnDate', None)
self.RefNumber = kwargs.pop('RefNumber', None)
self.Memo = kwargs.pop('Memo', None)
# [setattr(self, k, v) for k,v in kwargs.items()]

# List of dictionaries
self.line_item = []
self.line_expense = []
self._line_item = []
self._line_expense = []

def add_item(self, item):
"""
Takes a list of dictionaries or a single dictionary
"""
if isinstance(item, list):
[self.line_item.append(i) for i in item]
[self._line_item.append(i) for i in item]
else:
self.line_item.append(item)
self._line_item.append(item)

def add_expense(self, expense):
...
# """Add an expense item to a bill."""
# bill_line = {
# key : value for key, value in {
# }.items() if value is not None
# }
# self.line_expense.append(bill_line)

def create(self, conn):
"""
Push bill instance to QuickBooks.
"""Add an expense item to a bill."""
if isinstance(expense, list):
[self._line_expense.append(i) for i in expense]
else:
self._line_expense.append(expense)

def save(self, qb=''):
"""
If an item, and expense are required, the bill must exist before
the new insert can be preformed.
Overide the default method since it needs to work on an iterable.
Pop an item in the list and assign the current bill args to it.
"""
if len(self.line_item) > 0:
for i in self.line_item:
i["FQSaveToCache"] = as_decimal(1)
_line = process_insert(i , "BillItemLine")
conn.execute(_line, *i.values())

conn.execute(
process_insert(self.header, "Bill"), *list(self.header.values())
)

last_bill = conn.execute("sp_lastinsertid Bill").fetchall()[0][0]

if len(self.line_expense) > 0:
for i in self.line_expense:
expense = {"TxnID": last_bill, **i}
_line = process_insert(expense , "BillExpenseLine")
conn.execute(_line, *list(expense.values()))

# Output
return {
"vendor": self.header["VendorRefFullName"],
"ref_num": self.header["RefNumber"],
"id": last_bill
}

else:
for i in self.line_expense:
i["FQSaveToCache"] = as_decimal(1)
_line = process_insert(i , "BillExpenseLine")
conn.execute(_line, *i.values())

conn.execute(
process_insert(self.header, "Bill"), *list(self.header.values())
)

last_bill = conn.execute("sp_lastinsertid Bill").fetchall()[0][0]

# Output
try:
while self._line_item:
print('***', len(self._line_item), '***')

head = self._line_item.pop(0)
head.update_obj(self)

if len(self._line_item) == 0:
# Update FQSaveToCache == 0
head.FQSaveToCache = 0
qb.cursor.execute(head.create(), head.to_values())
else:
qb.cursor.execute(head.create(), head.to_values())

return {
"vendor": self.header["VendorRefFullName"],
"ref_num": self.header["RefNumber"],
"id": last_bill
'Vendor': self.VendorRefFullName,
'RefNum': self.RefNumber,
# Last insert on a parent table will still refrence an insert on the child table
"id": qb.last_insert(self.__table_name__)
}

def save(self, qb):

for i in self.line_item:


except Exception as e:
return {
"id": e.args[-1],
"key": self.RefNumber
}


@classmethod
def get_args(cls, con):
try:
Expand All @@ -141,5 +123,5 @@ def get_table(cls, con):
def __repr__(self):
return f"""
Bill: {self.VendorRefFullName}: {self.RefNumber}
Line Items: {len(self.line_item)} Expense Items: {len(self.line_expense)}
Line Items: {len(self._line_item)} Expense Items: {len(self._line_expense)}
"""
26 changes: 26 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[metadata]
name = qbodbc
version = 0.0.2
author = Bill Ash
author_email = [email protected]
description = SQL Wrapper for QuickBooks Desktop.
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/bill-ash/qbodbc
project_urls =
Bug Tracker = https://github.com/bill-ash/qbodbc/issues
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: MIT License
Operating System :: OS Independent

[options]
packages = find:
python_requires = >=3.6
install_requires =
pandas>=1.3.4
pyodbc>=4.0.32
[options.extras_require]
tests =
pytest

39 changes: 0 additions & 39 deletions setup.py

This file was deleted.

4 changes: 2 additions & 2 deletions tests/client/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def test_session():


def test_query():
session = QuickBooks(remote_dsn=TestConf.DSN)
session = QuickBooks(remote_dsn=TestConf.DSN, ip=TestConf.IP)
session.connect()

resp = session.sql('select * from account', [])
Expand Down Expand Up @@ -75,7 +75,7 @@ def test_methods():


def test_call():
con = QuickBooks("qbtest")
con = QuickBooks(TestConf.DSN)
con.connect()
account = con.query("SELECT * FROM Account")
assert len(account) > 1
Expand Down
Loading

0 comments on commit 1377559

Please sign in to comment.