Compare commits

...

21 commits

Author SHA1 Message Date
Lance Edgar bbded3272c docs: use markdown for readme file 2024-09-13 18:06:50 -05:00
Lance Edgar 5016ed9e64 fix: refactor grid usage per wuttaweb 2024-08-16 15:27:42 -05:00
Lance Edgar 38af9dd434 bump: version 0.1.7 → 0.2.0 2024-06-11 19:04:09 -05:00
Lance Edgar 6bb18b47c1 feat: switch from setup.cfg to pyproject.toml + hatchling 2024-06-11 19:04:00 -05:00
Lance Edgar 19bb5bd27f Fix default dist filename for release task
not sure why this fix was needed, did setuptools behavior change?
2024-06-03 11:26:32 -05:00
Lance Edgar d39a8a7adf Update changelog 2024-06-03 11:25:46 -05:00
Lance Edgar 06bc1d14c9 Add request object to vendor QB bank accounts grid 2024-04-30 19:34:15 -05:00
Lance Edgar 7e08dd0f89 Add support for Vendor -> Quickbooks Bank Accounts field
this feels a bit hacky yet, but had to introduce some new mechanisms
to allow for extra template stuff while avoiding adding a new
vendors/edit (etc.) template in this project, since we are using a
view supplement..ugh
2024-04-16 18:23:49 -05:00
Lance Edgar ddeb4545a6 Update changelog 2023-08-29 22:24:58 -05:00
Lance Edgar 831403c90a Mark exportable invoice as deleted, instead of actually deleting
and allow bulk-delete, but prevent edit
2023-08-09 18:06:08 -05:00
Lance Edgar 74bf66cd71 Update changelog 2023-06-01 14:30:39 -05:00
Lance Edgar 62f50328aa Replace setup.py contents with setup.cfg 2023-05-16 14:35:06 -05:00
Lance Edgar df93a573bf Stop passing newstyle kwarg to Form.validate()
no longer needed
2023-05-14 20:35:22 -05:00
Lance Edgar 92c817dfda Update changelog 2023-02-21 19:17:29 -06:00
Lance Edgar bc96879fe3 Show invoice amounts as currency 2023-02-21 18:49:15 -06:00
Lance Edgar 0b71e20f74 Update changelog 2023-02-20 21:55:55 -06:00
Lance Edgar 289b9dccf3 Fix fieldname for invoice view 2023-02-20 18:56:16 -06:00
Lance Edgar 18419fc861 Catch/display error when exporting QB invoices
don't let it raise to email alert
2023-02-20 18:33:14 -06:00
Lance Edgar 816874cc84 Refactor Query.get() => Session.get() per SQLAlchemy 1.4 2023-02-20 18:30:29 -06:00
Lance Edgar eaaf437568 Update changelog 2023-01-26 09:46:05 -06:00
Lance Edgar d7917f4034 Commit session when refreshing invoices 2023-01-26 09:45:22 -06:00
13 changed files with 422 additions and 132 deletions

3
.gitignore vendored
View file

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

View file

@ -5,6 +5,39 @@ 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/)
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
### Changed
- Only include "export invoice" logic if user has perm.

11
README.md Normal file
View file

@ -0,0 +1,11 @@
# 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.

View file

@ -1,14 +0,0 @@
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/

41
pyproject.toml Normal file
View file

@ -0,0 +1,41 @@
[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

View file

@ -1,95 +0,0 @@
# -*- 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,3 +1,9 @@
# -*- coding: utf-8; -*-
__version__ = '0.1.1'
try:
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)
}
TailboneGrid.methods.allChecked = function(checkedList) {
${grid.vue_component}.methods.allChecked = function(checkedList) {
// (de-)select all visible invoices when header checkbox is clicked
let checked = !!checkedList.length
this.toggleRows(this.allRowUUIDs(), checked)

View file

@ -0,0 +1,86 @@
## -*- 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

@ -0,0 +1,58 @@
## -*- 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,6 +54,8 @@ class ExportableInvoiceView(MasterView):
model_class = QuickbooksExportableInvoice
route_prefix = 'quickbooks.exportable_invoices'
url_prefix = '/quickbooks/exportable-invoices'
editable = False
bulk_deletable = True
has_versions = True
labels = {
@ -87,7 +89,7 @@ class ExportableInvoiceView(MasterView):
'supplies_amount',
'quickbooks_vendor_name',
'quickbooks_vendor_terms',
'quickbooks_vendor_bank_account',
'quickbooks_bank_account',
'quickbooks_export_template',
'status_code',
'status_text',
@ -147,8 +149,10 @@ class ExportableInvoiceView(MasterView):
g.set_sort_defaults('invoice_date', 'desc')
g.set_link('invoice_date')
# invoice_total
# currency fields
g.set_type('invoice_total', 'currency')
g.set_type('shipping_amount', 'currency')
g.set_type('supplies_amount', 'currency')
# status_code
g.set_enum('status_code', model.QuickbooksExportableInvoice.STATUS)
@ -169,6 +173,8 @@ class ExportableInvoiceView(MasterView):
g.check_all_handler = 'allChecked'
def grid_extra_class(self, invoice, i):
if invoice.deleted:
return 'warning'
if not self.exportable(invoice):
if invoice.status_code in (invoice.STATUS_DEPTS_IGNORED,
invoice.STATUS_EXPORTED,
@ -201,6 +207,22 @@ class ExportableInvoiceView(MasterView):
"""
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):
super(ExportableInvoiceView, self).configure_form(f)
model = self.model
@ -212,8 +234,10 @@ class ExportableInvoiceView(MasterView):
# vendor
f.set_renderer('vendor', self.render_vendor)
# invoice_total
# currency fields
f.set_type('invoice_total', 'currency')
f.set_type('shipping_amount', 'currency')
f.set_type('supplies_amount', 'currency')
# status
f.set_enum('status_code', model.QuickbooksExportableInvoice.STATUS)
@ -318,6 +342,7 @@ class ExportableInvoiceView(MasterView):
progress.session.save()
else:
session.commit()
if progress:
progress.session.load()
progress.session['complete'] = True
@ -335,13 +360,13 @@ class ExportableInvoiceView(MasterView):
model = self.model
form = forms.Form(schema=ToggleInvoices(), request=self.request)
if not form.validate(newstyle=True):
if not form.validate():
return {'error': "Form did not validate"}
uuids = form.validated['uuids'].split(',')
invoices = []
for uuid in uuids:
invoice = self.Session.query(model.QuickbooksExportableInvoice).get(uuid)
invoice = self.Session.get(model.QuickbooksExportableInvoice, uuid)
if invoice and self.exportable(invoice):
invoices.append(invoice)
if not invoices:
@ -363,13 +388,13 @@ class ExportableInvoiceView(MasterView):
model = self.model
form = forms.Form(schema=ToggleInvoices(), request=self.request)
if not form.validate(newstyle=True):
if not form.validate():
return {'error': "Form did not validate"}
uuids = form.validated['uuids'].split(',')
invoices = []
for uuid in uuids:
invoice = self.Session.query(model.QuickbooksExportableInvoice).get(uuid)
invoice = self.Session.get(model.QuickbooksExportableInvoice, uuid)
if invoice and self.exportable(invoice):
invoices.append(invoice)
if not invoices:
@ -398,7 +423,7 @@ class ExportableInvoiceView(MasterView):
invoices = []
for uuid in selected:
invoice = self.Session.query(model.QuickbooksExportableInvoice).get(uuid)
invoice = self.Session.get(model.QuickbooksExportableInvoice, uuid)
if invoice and invoice.status_code == invoice.STATUS_EXPORTABLE:
invoices.append(invoice)
else:
@ -410,7 +435,16 @@ class ExportableInvoiceView(MasterView):
return self.redirect(self.get_index_url())
# perform actual export and capture result
result = self.do_export(invoices)
try:
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
self.set_selected(set())

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2022 Lance Edgar
# Copyright © 2010-2024 Lance Edgar
#
# This file is part of Rattail.
#
@ -24,6 +24,14 @@
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
@ -44,14 +52,139 @@ class VendorViewSupplement(ViewSupplement):
g.set_filter('quickbooks_terms', model.QuickbooksVendor.quickbooks_terms)
def configure_form(self, f):
# quickbooks_name
f.append('quickbooks_name')
# 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')
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):
model = self.model
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):
VendorViewSupplement.defaults(config)

View file

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