Compare commits

..

No commits in common. "master" and "v0.1.1" have entirely different histories.

13 changed files with 132 additions and 422 deletions

3
.gitignore vendored
View file

@ -1,4 +1 @@
*~
*.pyc
dist/
tailbone_quickbooks.egg-info/ tailbone_quickbooks.egg-info/

View file

@ -5,39 +5,6 @@ All notable changes to tailbone-quickbooks will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## v0.2.0 (2024-06-11)
### Feat
- switch from setup.cfg to pyproject.toml + hatchling
## [0.1.7] - 2024-06-03
### Changed
- Add support for Vendor -> Quickbooks Bank Accounts field.
## [0.1.6] - 2023-08-29
### Changed
- Mark exportable invoice as deleted, instead of actually deleting.
## [0.1.5] - 2023-06-01
### Changed
- Stop passing `newstyle` kwarg to `Form.validate()`.
- Replace `setup.py` contents with `setup.cfg`.
## [0.1.4] - 2023-02-21
### Changed
- Show invoice amounts as currency.
## [0.1.3] - 2023-02-20
### Changed
- Refactor `Query.get()` => `Session.get()` per SQLAlchemy 1.4.
- Catch/display error when exporting QB invoices.
- Fix fieldname for invoice view.
## [0.1.2] - 2023-01-26
### Changed
- Commit session when refreshing invoices.
## [0.1.1] - 2023-01-25 ## [0.1.1] - 2023-01-25
### Changed ### Changed
- Only include "export invoice" logic if user has perm. - Only include "export invoice" logic if user has perm.

View file

@ -1,11 +0,0 @@
# tailbone-quickbooks
Rattail is a retail software framework, released under the GNU General
Public License.
This package contains software interfaces for
[Quickbooks](https://quickbooks.intuit.com/).
Please see the [Rattail Project](https://rattailproject.org/) for more
information.

14
README.rst Normal file
View file

@ -0,0 +1,14 @@
tailbone-quickbooks
===================
Rattail is a retail software framework, released under the GNU General
Public License.
This package contains software interfaces for `Quickbooks`_.
.. _`Quickbooks`: https://quickbooks.intuit.com/
Please see the `Rattail Project`_ for more information.
.. _`Rattail Project`: https://rattailproject.org/

View file

@ -1,41 +0,0 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "tailbone-quickbooks"
version = "0.2.0"
description = "Tailbone integration package for Quickbooks"
readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
license = {text = "GNU GPL v3+"}
classifiers = [
"Development Status :: 3 - Alpha",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Topic :: Office/Business",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"rattail-quickbooks",
"Tailbone",
]
[project.urls]
Homepage = "https://rattailproject.org"
Repository = "https://kallithea.rattailproject.org/rattail-project/tailbone-quickbooks"
Changelog = "https://kallithea.rattailproject.org/rattail-project/tailbone-quickbooks/files/master/CHANGELOG.md"
[tool.commitizen]
version_provider = "pep621"
tag_format = "v$version"
update_changelog_on_bump = true

95
setup.py Normal file
View file

@ -0,0 +1,95 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2022 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
tailbone-quickbooks setup script
"""
import os
from setuptools import setup, find_packages
here = os.path.abspath(os.path.dirname(__file__))
exec(open(os.path.join(here, 'tailbone_quickbooks', '_version.py')).read())
README = open(os.path.join(here, 'README.rst')).read()
requires = [
#
# Version numbers within comments below have specific meanings.
# Basically the 'low' value is a "soft low," and 'high' a "soft high."
# In other words:
#
# If either a 'low' or 'high' value exists, the primary point to be
# made about the value is that it represents the most current (stable)
# version available for the package (assuming typical public access
# methods) whenever this project was started and/or documented.
# Therefore:
#
# If a 'low' version is present, you should know that attempts to use
# versions of the package significantly older than the 'low' version
# may not yield happy results. (A "hard" high limit may or may not be
# indicated by a true version requirement.)
#
# Similarly, if a 'high' version is present, and especially if this
# project has laid dormant for a while, you may need to refactor a bit
# when attempting to support a more recent version of the package. (A
# "hard" low limit should be indicated by a true version requirement
# when a 'high' version is present.)
#
# In any case, developers and other users are encouraged to play
# outside the lines with regard to these soft limits. If bugs are
# encountered then they should be filed as such.
#
# package # low high
'rattail-quickbooks', # 0.1.0
'Tailbone', # 0.8.199
]
setup(
name = "tailbone-quickbooks",
version = __version__,
author = "Lance Edgar",
author_email = "lance@edbob.org",
url = "https://rattailproject.org/",
description = "Tailbone integration package for Quickbooks",
long_description = README,
classifiers = [
'Development Status :: 3 - Alpha',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Topic :: Office/Business',
'Topic :: Software Development :: Libraries :: Python Modules',
],
install_requires = requires,
packages = find_packages(),
include_package_data = True,
)

View file

@ -1,9 +1,3 @@
# -*- coding: utf-8; -*- # -*- coding: utf-8; -*-
try: __version__ = '0.1.1'
from importlib.metadata import version
except ImportError:
from importlib_metadata import version
__version__ = version('tailbone-quickbooks')

View file

@ -97,7 +97,7 @@
this.toggleRows(uuids, checked) this.toggleRows(uuids, checked)
} }
${grid.vue_component}.methods.allChecked = function(checkedList) { TailboneGrid.methods.allChecked = function(checkedList) {
// (de-)select all visible invoices when header checkbox is clicked // (de-)select all visible invoices when header checkbox is clicked
let checked = !!checkedList.length let checked = !!checkedList.length
this.toggleRows(this.allRowUUIDs(), checked) this.toggleRows(this.allRowUUIDs(), checked)

View file

@ -1,86 +0,0 @@
## -*- coding: utf-8; -*-
<script type="text/javascript">
${form.component_studly}Data.quickbooksBankAccountShowDialog = false
${form.component_studly}Data.quickbooksBankAccountEditing = null
${form.component_studly}Data.quickbooksBankAccountStore = null
${form.component_studly}Data.quickbooksBankAccountNumber = null
${form.component_studly}Data.quickbooksBankAccountStoreOptions = ${json.dumps(store_options)|n}
${form.component_studly}.methods.quickbooksBankAccountCreate = function() {
this.quickbooksBankAccountEditing = null
this.quickbooksBankAccountStore = null
this.quickbooksBankAccountNumber = null
this.quickbooksBankAccountShowDialog = true
}
${form.component_studly}.methods.quickbooksBankAccountEdit = function(row) {
this.quickbooksBankAccountEditing = row
this.quickbooksBankAccountStore = row.store_uuid
this.quickbooksBankAccountNumber = row.account_number
this.quickbooksBankAccountShowDialog = true
this.$nextTick(() => {
this.$refs.quickbooksBankAccountNumber.focus()
})
}
${form.component_studly}.computed.quickbooksBankAccountSaveDisabled = function(row) {
if (!this.quickbooksBankAccountStore) {
return true
}
if (!this.quickbooksBankAccountNumber) {
return true
}
return false
}
${form.component_studly}.computed.quickbooksBankAccountsFinal = function() {
return JSON.stringify(this.quickbooksBankAccountsData)
}
${form.component_studly}.methods.quickbooksBankAccountSave = function() {
if (this.quickbooksBankAccountEditing) {
this.quickbooksBankAccountEditing.store_uuid = this.quickbooksBankAccountStore
this.quickbooksBankAccountEditing.account_number = this.quickbooksBankAccountNumber
this.quickbooksBankAccountShowDialog = false
} else {
if (this.quickbooksBankAccountIsStoreDefined(this.quickbooksBankAccountStore)) {
alert("An account number is already defined for that store!")
} else {
this.quickbooksBankAccountsData.push({
store_uuid: this.quickbooksBankAccountStore,
store: this.quickbooksBankAccountGetStoreDisplay(this.quickbooksBankAccountStore),
account_number: this.quickbooksBankAccountNumber,
})
this.quickbooksBankAccountShowDialog = false
}
}
}
${form.component_studly}.methods.quickbooksBankAccountIsStoreDefined = function(uuid) {
for (let account of this.quickbooksBankAccountsData) {
if (account.store_uuid == uuid) {
return true
}
}
return false
}
${form.component_studly}.methods.quickbooksBankAccountGetStoreDisplay = function(uuid) {
for (let store of this.quickbooksBankAccountStoreOptions) {
if (store.uuid == uuid) {
return store.display
}
}
}
${form.component_studly}.methods.quickbooksBankAccountDelete = function(row) {
if (confirm("Really delete this account number?")) {
let i = this.quickbooksBankAccountsData.indexOf(row)
this.quickbooksBankAccountsData.splice(i, 1)
this.quickbooksBankAccountShowDialog = false
}
}
</script>

View file

@ -1,58 +0,0 @@
## -*- coding: utf-8; -*-
<div style="display: flex;">
<span style="flex-grow: 1;"></span>
<b-button type="is-primary"
icon-pack="fas"
icon-left="plus"
@click="quickbooksBankAccountCreate()">
Add Account
</b-button>
</div>
${grid.render_table_element(data_prop='quickbooksBankAccountsData')|n}
<b-modal has-modal-card
:active.sync="quickbooksBankAccountShowDialog">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Quickbooks Bank Account</p>
</header>
<section class="modal-card-body">
<b-field label="Store"
:type="{'is-danger': !quickbooksBankAccountStore}">
<b-select v-model="quickbooksBankAccountStore"
:disabled="quickbooksBankAccountEditing">
<option v-for="store in quickbooksBankAccountStoreOptions"
:key="store.uuid"
:value="store.uuid">
{{ store.display }}
</option>
</b-select>
</b-field>
<b-field label="Account Number"
:type="{'is-danger': !quickbooksBankAccountNumber}">
<b-input v-model="quickbooksBankAccountNumber"
ref="quickbooksBankAccountNumber" />
</b-field>
</section>
<footer class="modal-card-foot">
<b-button type="is-primary"
@click="quickbooksBankAccountSave()"
:disabled="quickbooksBankAccountSaveDisabled"
icon-pack="fas"
icon-left="save">
Update
</b-button>
<b-button @click="quickbooksBankAccountShowDialog = false">
Cancel
</b-button>
</footer>
</div>
</b-modal>

View file

@ -54,8 +54,6 @@ class ExportableInvoiceView(MasterView):
model_class = QuickbooksExportableInvoice model_class = QuickbooksExportableInvoice
route_prefix = 'quickbooks.exportable_invoices' route_prefix = 'quickbooks.exportable_invoices'
url_prefix = '/quickbooks/exportable-invoices' url_prefix = '/quickbooks/exportable-invoices'
editable = False
bulk_deletable = True
has_versions = True has_versions = True
labels = { labels = {
@ -89,7 +87,7 @@ class ExportableInvoiceView(MasterView):
'supplies_amount', 'supplies_amount',
'quickbooks_vendor_name', 'quickbooks_vendor_name',
'quickbooks_vendor_terms', 'quickbooks_vendor_terms',
'quickbooks_bank_account', 'quickbooks_vendor_bank_account',
'quickbooks_export_template', 'quickbooks_export_template',
'status_code', 'status_code',
'status_text', 'status_text',
@ -149,10 +147,8 @@ class ExportableInvoiceView(MasterView):
g.set_sort_defaults('invoice_date', 'desc') g.set_sort_defaults('invoice_date', 'desc')
g.set_link('invoice_date') g.set_link('invoice_date')
# currency fields # invoice_total
g.set_type('invoice_total', 'currency') g.set_type('invoice_total', 'currency')
g.set_type('shipping_amount', 'currency')
g.set_type('supplies_amount', 'currency')
# status_code # status_code
g.set_enum('status_code', model.QuickbooksExportableInvoice.STATUS) g.set_enum('status_code', model.QuickbooksExportableInvoice.STATUS)
@ -173,8 +169,6 @@ class ExportableInvoiceView(MasterView):
g.check_all_handler = 'allChecked' g.check_all_handler = 'allChecked'
def grid_extra_class(self, invoice, i): def grid_extra_class(self, invoice, i):
if invoice.deleted:
return 'warning'
if not self.exportable(invoice): if not self.exportable(invoice):
if invoice.status_code in (invoice.STATUS_DEPTS_IGNORED, if invoice.status_code in (invoice.STATUS_DEPTS_IGNORED,
invoice.STATUS_EXPORTED, invoice.STATUS_EXPORTED,
@ -207,22 +201,6 @@ class ExportableInvoiceView(MasterView):
""" """
return invoice.status_code == invoice.STATUS_EXPORTABLE return invoice.status_code == invoice.STATUS_EXPORTABLE
def deletable_instance(self, invoice):
if invoice.deleted:
return False
if invoice.exported:
return False
return True
def delete_instance(self, invoice):
app = self.get_rattail_app()
session = app.get_session(invoice)
invoice.deleted = app.make_utc()
# nb. when bulk-deleting, user is in different session
invoice.deleted_by = session.merge(self.request.user)
invoice.status_code = invoice.STATUS_DELETED
session.flush()
def configure_form(self, f): def configure_form(self, f):
super(ExportableInvoiceView, self).configure_form(f) super(ExportableInvoiceView, self).configure_form(f)
model = self.model model = self.model
@ -234,10 +212,8 @@ class ExportableInvoiceView(MasterView):
# vendor # vendor
f.set_renderer('vendor', self.render_vendor) f.set_renderer('vendor', self.render_vendor)
# currency fields # invoice_total
f.set_type('invoice_total', 'currency') f.set_type('invoice_total', 'currency')
f.set_type('shipping_amount', 'currency')
f.set_type('supplies_amount', 'currency')
# status # status
f.set_enum('status_code', model.QuickbooksExportableInvoice.STATUS) f.set_enum('status_code', model.QuickbooksExportableInvoice.STATUS)
@ -342,7 +318,6 @@ class ExportableInvoiceView(MasterView):
progress.session.save() progress.session.save()
else: else:
session.commit()
if progress: if progress:
progress.session.load() progress.session.load()
progress.session['complete'] = True progress.session['complete'] = True
@ -360,13 +335,13 @@ class ExportableInvoiceView(MasterView):
model = self.model model = self.model
form = forms.Form(schema=ToggleInvoices(), request=self.request) form = forms.Form(schema=ToggleInvoices(), request=self.request)
if not form.validate(): if not form.validate(newstyle=True):
return {'error': "Form did not validate"} return {'error': "Form did not validate"}
uuids = form.validated['uuids'].split(',') uuids = form.validated['uuids'].split(',')
invoices = [] invoices = []
for uuid in uuids: for uuid in uuids:
invoice = self.Session.get(model.QuickbooksExportableInvoice, uuid) invoice = self.Session.query(model.QuickbooksExportableInvoice).get(uuid)
if invoice and self.exportable(invoice): if invoice and self.exportable(invoice):
invoices.append(invoice) invoices.append(invoice)
if not invoices: if not invoices:
@ -388,13 +363,13 @@ class ExportableInvoiceView(MasterView):
model = self.model model = self.model
form = forms.Form(schema=ToggleInvoices(), request=self.request) form = forms.Form(schema=ToggleInvoices(), request=self.request)
if not form.validate(): if not form.validate(newstyle=True):
return {'error': "Form did not validate"} return {'error': "Form did not validate"}
uuids = form.validated['uuids'].split(',') uuids = form.validated['uuids'].split(',')
invoices = [] invoices = []
for uuid in uuids: for uuid in uuids:
invoice = self.Session.get(model.QuickbooksExportableInvoice, uuid) invoice = self.Session.query(model.QuickbooksExportableInvoice).get(uuid)
if invoice and self.exportable(invoice): if invoice and self.exportable(invoice):
invoices.append(invoice) invoices.append(invoice)
if not invoices: if not invoices:
@ -423,7 +398,7 @@ class ExportableInvoiceView(MasterView):
invoices = [] invoices = []
for uuid in selected: for uuid in selected:
invoice = self.Session.get(model.QuickbooksExportableInvoice, uuid) invoice = self.Session.query(model.QuickbooksExportableInvoice).get(uuid)
if invoice and invoice.status_code == invoice.STATUS_EXPORTABLE: if invoice and invoice.status_code == invoice.STATUS_EXPORTABLE:
invoices.append(invoice) invoices.append(invoice)
else: else:
@ -435,16 +410,7 @@ class ExportableInvoiceView(MasterView):
return self.redirect(self.get_index_url()) return self.redirect(self.get_index_url())
# perform actual export and capture result # perform actual export and capture result
try: result = self.do_export(invoices)
result = self.do_export(invoices)
except Exception as error:
log.warning("failed to export invoices: %s",
[inv.uuid for inv in invoices],
exc_info=True)
self.request.session.flash(simple_error(error), 'error')
return self.redirect(self.request.get_referrer(
default=self.get_index_url()))
# clear out current checkbox selection # clear out current checkbox selection
self.set_selected(set()) self.set_selected(set())

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2024 Lance Edgar # Copyright © 2010-2022 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -24,14 +24,6 @@
Vendor views, w/ Quickbooks integration Vendor views, w/ Quickbooks integration
""" """
import json
import colander
from deform import widget as dfwidget
from pyramid.renderers import render
from webhelpers2.html import HTML, tags
from tailbone import grids
from tailbone.views import ViewSupplement from tailbone.views import ViewSupplement
@ -52,139 +44,14 @@ class VendorViewSupplement(ViewSupplement):
g.set_filter('quickbooks_terms', model.QuickbooksVendor.quickbooks_terms) g.set_filter('quickbooks_terms', model.QuickbooksVendor.quickbooks_terms)
def configure_form(self, f): def configure_form(self, f):
# quickbooks_name
f.append('quickbooks_name') f.append('quickbooks_name')
# quickbooks_bank_account
f.append('quickbooks_bank_account') f.append('quickbooks_bank_account')
# quickbooks_bank_accounts
f.append('quickbooks_bank_accounts_')
f.set_renderer('quickbooks_bank_accounts_', self.render_quickbooks_bank_accounts)
f.set_node('quickbooks_bank_accounts_', BankAccounts())
f.set_widget('quickbooks_bank_accounts_', BankAccountsWidget(request=self.request))
# quickbooks_terms
f.append('quickbooks_terms') f.append('quickbooks_terms')
def render_quickbooks_bank_accounts(self, vendor, field):
accounts = getattr(vendor, 'quickbooks_bank_accounts')
if accounts:
g = make_accounts_grid(self.request)
return HTML.literal(g.render_table_element(data_prop='quickbooksBankAccountsData'))
def objectify(self, vendor, form, data):
model = self.model
old_accounts = vendor.quickbooks_bank_accounts
new_accounts = data['quickbooks_bank_accounts_']
for new_account in new_accounts:
old_account = old_accounts.get(new_account['store_uuid'])
if old_account:
if old_account.account_number != new_account['account_number']:
old_account.account_number = new_account['account_number']
else:
account = model.QuickbooksVendorBankAccount()
account.store_uuid = new_account['store_uuid']
account.account_number = new_account['account_number']
vendor.quickbooks_bank_accounts[account.store_uuid] = account
self.Session.add(account)
final_store_uuids = set([a['store_uuid'] for a in new_accounts])
for old_account in list(vendor.quickbooks_bank_accounts.values()):
if old_account.store_uuid not in final_store_uuids:
self.Session.delete(old_account)
return vendor
def template_kwargs(self, **kwargs):
app = self.get_rattail_app()
form = kwargs.get('form')
if form:
# quickbooks bank accounts
vendor = kwargs['instance']
accounts = []
for account in vendor.quickbooks_bank_accounts.values():
store = account.store
accounts.append({
'uuid': account.uuid,
'store': f'{store.id} - {store.name}',
'store_uuid': store.uuid,
'store_id': store.id,
'store_name': store.name,
'account_number': account.account_number,
})
accounts.sort(key=lambda a: a['store_id'])
# nb. this is needed for widget *and* readonly template
form.set_json_data('quickbooksBankAccountsData', accounts)
# TODO: these are needed by widget
stores = []
for store in app.get_active_stores(self.Session()):
stores.append({
'uuid': store.uuid,
'display': f'{store.id} - {store.name}',
})
form.include_template('/vendors/quickbooks_bank_accounts_js.mako', {
'store_options': stores,
})
return kwargs
def get_version_child_classes(self): def get_version_child_classes(self):
model = self.model model = self.model
return [model.QuickbooksVendor] return [model.QuickbooksVendor]
def make_accounts_grid(request):
g = grids.Grid(request,
key='quickbooks_bank_accounts',
data=[], # empty data
columns=[
'store',
'account_number',
])
return g
class BankAccount(colander.MappingSchema):
store_uuid = colander.SchemaNode(colander.String())
account_number = colander.SchemaNode(colander.String())
class BankAccounts(colander.SequenceSchema):
account = BankAccount()
class BankAccountsWidget(dfwidget.Widget):
def serialize(self, field, cstruct, **kw):
g = make_accounts_grid(self.request)
g.actions.append(
grids.GridAction(self.request, 'edit', icon='edit',
click_handler='quickbooksBankAccountEdit(props.row)'))
g.actions.append(
grids.GridAction(self.request, 'delete', icon='trash',
click_handler='quickbooksBankAccountDelete(props.row)'))
widget = render('/vendors/quickbooks_bank_accounts_widget.mako', {
'grid': g,
})
return HTML.tag('div', c=[
HTML.literal(widget),
tags.hidden(field.name, **{':value': "quickbooksBankAccountsFinal"}),
])
def deserialize(self, field, pstruct):
return json.loads(pstruct)
def includeme(config): def includeme(config):
VendorViewSupplement.defaults(config) VendorViewSupplement.defaults(config)

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2024 Lance Edgar # Copyright © 2010-2022 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -24,18 +24,24 @@
Tasks for tailbone-quickbooks Tasks for tailbone-quickbooks
""" """
import os
from invoke import task from invoke import task
here = os.path.abspath(os.path.dirname(__file__))
exec(open(os.path.join(here, 'tailbone_quickbooks', '_version.py')).read())
@task @task
def release(c): def release(c):
""" """
Release a new version of tailbone-quickbooks Release a new version of tailbone-quickbooks
""" """
# rebuild package # rebuild local tar.gz file for distribution
c.run('rm -rf dist')
c.run('rm -rf tailbone_quickbooks.egg-info') c.run('rm -rf tailbone_quickbooks.egg-info')
c.run('python -m build --sdist') c.run('python setup.py sdist --formats=gztar')
# upload to PyPI # upload to PyPI
c.run('twine upload dist/*') filename = 'tailbone-quickbooks-{}.tar.gz'.format(__version__)
c.run('twine upload dist/{}'.format(filename))