diff --git a/.gitignore b/.gitignore index ea1abef..2e6159c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1 @@ -*~ -*.pyc -dist/ tailbone_quickbooks.egg-info/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 72cab08..99cac9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/) 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. diff --git a/README.md b/README.md deleted file mode 100644 index 8a0c37d..0000000 --- a/README.md +++ /dev/null @@ -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. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..343f4bf --- /dev/null +++ b/README.rst @@ -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/ diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index c3a0bb4..0000000 --- a/pyproject.toml +++ /dev/null @@ -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 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..03eed1f --- /dev/null +++ b/setup.py @@ -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 . +# +################################################################################ +""" +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, +) diff --git a/tailbone_quickbooks/_version.py b/tailbone_quickbooks/_version.py index 4905e5d..4984097 100644 --- a/tailbone_quickbooks/_version.py +++ b/tailbone_quickbooks/_version.py @@ -1,9 +1,3 @@ # -*- coding: utf-8; -*- -try: - from importlib.metadata import version -except ImportError: - from importlib_metadata import version - - -__version__ = version('tailbone-quickbooks') +__version__ = '0.1.1' diff --git a/tailbone_quickbooks/templates/quickbooks/exportable-invoices/index.mako b/tailbone_quickbooks/templates/quickbooks/exportable-invoices/index.mako index 0469f32..dc6ec4f 100644 --- a/tailbone_quickbooks/templates/quickbooks/exportable-invoices/index.mako +++ b/tailbone_quickbooks/templates/quickbooks/exportable-invoices/index.mako @@ -97,7 +97,7 @@ 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 let checked = !!checkedList.length this.toggleRows(this.allRowUUIDs(), checked) diff --git a/tailbone_quickbooks/templates/vendors/quickbooks_bank_accounts_js.mako b/tailbone_quickbooks/templates/vendors/quickbooks_bank_accounts_js.mako deleted file mode 100644 index 109bb9e..0000000 --- a/tailbone_quickbooks/templates/vendors/quickbooks_bank_accounts_js.mako +++ /dev/null @@ -1,86 +0,0 @@ -## -*- coding: utf-8; -*- - - diff --git a/tailbone_quickbooks/templates/vendors/quickbooks_bank_accounts_widget.mako b/tailbone_quickbooks/templates/vendors/quickbooks_bank_accounts_widget.mako deleted file mode 100644 index 32a6dbe..0000000 --- a/tailbone_quickbooks/templates/vendors/quickbooks_bank_accounts_widget.mako +++ /dev/null @@ -1,58 +0,0 @@ -## -*- coding: utf-8; -*- - -
- - - Add Account - -
- -${grid.render_table_element(data_prop='quickbooksBankAccountsData')|n} - - - - diff --git a/tailbone_quickbooks/views/quickbooks/invoices.py b/tailbone_quickbooks/views/quickbooks/invoices.py index c8ab414..a70a8ec 100644 --- a/tailbone_quickbooks/views/quickbooks/invoices.py +++ b/tailbone_quickbooks/views/quickbooks/invoices.py @@ -54,8 +54,6 @@ 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 = { @@ -89,7 +87,7 @@ class ExportableInvoiceView(MasterView): 'supplies_amount', 'quickbooks_vendor_name', 'quickbooks_vendor_terms', - 'quickbooks_bank_account', + 'quickbooks_vendor_bank_account', 'quickbooks_export_template', 'status_code', 'status_text', @@ -149,10 +147,8 @@ class ExportableInvoiceView(MasterView): g.set_sort_defaults('invoice_date', 'desc') g.set_link('invoice_date') - # currency fields + # invoice_total 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) @@ -173,8 +169,6 @@ 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, @@ -207,22 +201,6 @@ 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 @@ -234,10 +212,8 @@ class ExportableInvoiceView(MasterView): # vendor f.set_renderer('vendor', self.render_vendor) - # currency fields + # invoice_total 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) @@ -342,7 +318,6 @@ class ExportableInvoiceView(MasterView): progress.session.save() else: - session.commit() if progress: progress.session.load() progress.session['complete'] = True @@ -360,13 +335,13 @@ class ExportableInvoiceView(MasterView): model = self.model form = forms.Form(schema=ToggleInvoices(), request=self.request) - if not form.validate(): + if not form.validate(newstyle=True): return {'error': "Form did not validate"} uuids = form.validated['uuids'].split(',') invoices = [] 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): invoices.append(invoice) if not invoices: @@ -388,13 +363,13 @@ class ExportableInvoiceView(MasterView): model = self.model form = forms.Form(schema=ToggleInvoices(), request=self.request) - if not form.validate(): + if not form.validate(newstyle=True): return {'error': "Form did not validate"} uuids = form.validated['uuids'].split(',') invoices = [] 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): invoices.append(invoice) if not invoices: @@ -423,7 +398,7 @@ class ExportableInvoiceView(MasterView): invoices = [] 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: invoices.append(invoice) else: @@ -435,16 +410,7 @@ class ExportableInvoiceView(MasterView): return self.redirect(self.get_index_url()) # perform actual export and capture result - 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())) + result = self.do_export(invoices) # clear out current checkbox selection self.set_selected(set()) diff --git a/tailbone_quickbooks/views/vendors.py b/tailbone_quickbooks/views/vendors.py index 0ce3230..7c3e70b 100644 --- a/tailbone_quickbooks/views/vendors.py +++ b/tailbone_quickbooks/views/vendors.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2024 Lance Edgar +# Copyright © 2010-2022 Lance Edgar # # This file is part of Rattail. # @@ -24,14 +24,6 @@ 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 @@ -52,139 +44,14 @@ 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) diff --git a/tasks.py b/tasks.py index 2a3f1e0..9dbdd44 100644 --- a/tasks.py +++ b/tasks.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2024 Lance Edgar +# Copyright © 2010-2022 Lance Edgar # # This file is part of Rattail. # @@ -24,18 +24,24 @@ 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 package - c.run('rm -rf dist') + # rebuild local tar.gz file for distribution 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 - c.run('twine upload dist/*') + filename = 'tailbone-quickbooks-{}.tar.gz'.format(__version__) + c.run('twine upload dist/{}'.format(filename))