Initial views, and basic invoice export UI tools
This commit is contained in:
commit
7384bd1fea
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
tailbone_quickbooks.egg-info/
|
10
CHANGELOG.md
Normal file
10
CHANGELOG.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
|
||||
# Changelog
|
||||
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).
|
||||
|
||||
## [0.1.0] - ??
|
||||
### Added
|
||||
- Initial version.
|
3
MANIFEST.in
Normal file
3
MANIFEST.in
Normal file
|
@ -0,0 +1,3 @@
|
|||
include *.md
|
||||
include *.rst
|
||||
recursive-include tailbone_quickbooks/templates *.mako
|
14
README.rst
Normal file
14
README.rst
Normal 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/
|
95
setup.py
Normal file
95
setup.py
Normal 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,
|
||||
)
|
27
tailbone_quickbooks/__init__.py
Normal file
27
tailbone_quickbooks/__init__.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# -*- 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 package root
|
||||
"""
|
||||
|
||||
from ._version import __version__
|
3
tailbone_quickbooks/_version.py
Normal file
3
tailbone_quickbooks/_version.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
__version__ = '0.1.0'
|
|
@ -0,0 +1,88 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/index.mako" />
|
||||
|
||||
<%def name="grid_tools()">
|
||||
${parent.grid_tools()}
|
||||
${h.form(url('{}.export'.format(route_prefix)), **{'@submit': 'exportingInvoices = true'})}
|
||||
${h.csrf_token(request)}
|
||||
% if master.has_perm('export'):
|
||||
<b-button type="is-primary"
|
||||
native-type="submit"
|
||||
icon-pack="fas"
|
||||
icon-left="arrow-circle-right"
|
||||
:disabled="exportingInvoices || !exportingInvoicesCount">
|
||||
{{ exportingInvoices ? "Working, please wait..." : `Export ${'$'}{this.exportingInvoicesCount} Invoices` }}
|
||||
</b-button>
|
||||
% endif
|
||||
${h.end_form()}
|
||||
</%def>
|
||||
|
||||
<%def name="modify_this_page_vars()">
|
||||
${parent.modify_this_page_vars()}
|
||||
<script type="text/javascript">
|
||||
|
||||
${grid.component_studly}Data.exportingInvoices = false
|
||||
${grid.component_studly}Data.exportingInvoicesCount = ${json.dumps(len(selected))|n}
|
||||
|
||||
${grid.component_studly}.methods.toggleRows = function(uuids, checked) {
|
||||
this.loading = true
|
||||
|
||||
let url = checked ? '${url('{}.select'.format(route_prefix))}' : '${url('{}.deselect'.format(route_prefix))}'
|
||||
let params = {
|
||||
uuids: uuids.join(','),
|
||||
}
|
||||
|
||||
this.submitForm(url, params, response => {
|
||||
this.exportingInvoicesCount = response.data.selected_count
|
||||
this.loading = false
|
||||
}, response => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
${grid.component_studly}.methods.rowChecked = function(checkedList, row) {
|
||||
// skip this logic if triggered by header ("all") checkbox
|
||||
if (!row) {
|
||||
return
|
||||
}
|
||||
|
||||
// are we toggling ON or OFF?
|
||||
let checked = checkedList.includes(row)
|
||||
|
||||
// collect all currently "checked" uuids
|
||||
let checkedUUIDs = []
|
||||
for (row of checkedList) {
|
||||
checkedUUIDs.push(row.uuid)
|
||||
}
|
||||
|
||||
// even though we are given only the one row which was associated with
|
||||
// the event, it is possible the user did a shift+click operation. and
|
||||
// since we cannot know for sure, we must assume so, which means we
|
||||
// must send "all" relevant uuids as we are not given the specific
|
||||
// range involved for shift+click
|
||||
let uuids = []
|
||||
if (checked) {
|
||||
// if toggling ON then we must send all "checked" rows
|
||||
uuids = checkedUUIDs
|
||||
} else {
|
||||
// if toggling OFF then we must send all "unchecked" rows
|
||||
for (uuid of this.allRowUUIDs()) {
|
||||
if (!checkedUUIDs.includes(uuid)) {
|
||||
uuids.push(uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.toggleRows(uuids, checked)
|
||||
}
|
||||
|
||||
TailboneGrid.methods.allChecked = function(checkedList) {
|
||||
// (de-)select all visible invoices when header checkbox is clicked
|
||||
let checked = !!checkedList.length
|
||||
this.toggleRows(this.allRowUUIDs(), checked)
|
||||
}
|
||||
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
|
||||
${parent.body()}
|
|
@ -0,0 +1,29 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/view.mako" />
|
||||
|
||||
<%def name="render_form_buttons()">
|
||||
% if master.has_perm('export'):
|
||||
${h.form(master.get_action_url('refresh', instance), **{'@submit': 'refreshingInvoice = true'})}
|
||||
${h.csrf_token(request)}
|
||||
<b-button type="is-primary"
|
||||
native-type="submit"
|
||||
icon-pack="fas"
|
||||
icon-left="redo"
|
||||
:disabled="refreshingInvoice">
|
||||
{{ refreshingInvoice ? "Working, please wait..." : "Refresh Data" }}
|
||||
</b-button>
|
||||
${h.end_form()}
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="modify_this_page_vars()">
|
||||
${parent.modify_this_page_vars()}
|
||||
<script type="text/javascript">
|
||||
|
||||
${form.component_studly}Data.refreshingInvoice = false
|
||||
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
|
||||
${parent.body()}
|
|
@ -0,0 +1,11 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/view.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('quickbooks.exportable_invoices.list'):
|
||||
<li>${h.link_to("Back to Exportable Invoices", url('quickbooks.exportable_invoices'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
|
||||
${parent.body()}
|
30
tailbone_quickbooks/views/__init__.py
Normal file
30
tailbone_quickbooks/views/__init__.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Views w/ Quickbooks integration
|
||||
"""
|
||||
|
||||
def includeme(config):
|
||||
config.include('tailbone_quickbooks.views.departments')
|
||||
config.include('tailbone_quickbooks.views.stores')
|
||||
config.include('tailbone_quickbooks.views.vendors')
|
55
tailbone_quickbooks/views/departments.py
Normal file
55
tailbone_quickbooks/views/departments.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Department views, w/ Quickbooks integration
|
||||
"""
|
||||
|
||||
from tailbone.views import ViewSupplement
|
||||
|
||||
|
||||
class DepartmentViewSupplement(ViewSupplement):
|
||||
"""
|
||||
Department view supplement for Quickbooks integration
|
||||
"""
|
||||
route_prefix = 'departments'
|
||||
|
||||
def get_grid_query(self, query):
|
||||
model = self.model
|
||||
return query.outerjoin(model.QuickbooksDepartment)
|
||||
|
||||
def configure_grid(self, g):
|
||||
model = self.model
|
||||
g.set_filter('quickbooks_expense_account', model.QuickbooksDepartment.quickbooks_expense_account)
|
||||
g.set_filter('quickbooks_expense_class', model.QuickbooksDepartment.quickbooks_expense_class)
|
||||
|
||||
def configure_form(self, f):
|
||||
f.append('quickbooks_expense_account')
|
||||
f.append('quickbooks_expense_class')
|
||||
|
||||
def get_version_child_classes(self):
|
||||
model = self.model
|
||||
return [model.QuickbooksDepartment]
|
||||
|
||||
|
||||
def includeme(config):
|
||||
DepartmentViewSupplement.defaults(config)
|
0
tailbone_quickbooks/views/quickbooks/__init__.py
Normal file
0
tailbone_quickbooks/views/quickbooks/__init__.py
Normal file
506
tailbone_quickbooks/views/quickbooks/invoices.py
Normal file
506
tailbone_quickbooks/views/quickbooks/invoices.py
Normal file
|
@ -0,0 +1,506 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Views for Quickbooks invoices
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from rattail_quickbooks.db.model import (QuickbooksExportableInvoice,
|
||||
QuickbooksExportableInvoiceDistribution,
|
||||
QuickbooksInvoiceExport)
|
||||
from rattail.util import simple_error
|
||||
|
||||
import colander
|
||||
from pyramid.httpexceptions import HTTPFound
|
||||
|
||||
from tailbone import forms
|
||||
from tailbone.views import MasterView
|
||||
from tailbone.views.exports import ExportMasterView
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ToggleInvoices(colander.MappingSchema):
|
||||
uuids = colander.SchemaNode(colander.String())
|
||||
|
||||
|
||||
class ExportableInvoiceView(MasterView):
|
||||
"""
|
||||
Master view for Quickbooks exportable invoices
|
||||
"""
|
||||
model_class = QuickbooksExportableInvoice
|
||||
route_prefix = 'quickbooks.exportable_invoices'
|
||||
url_prefix = '/quickbooks/exportable-invoices'
|
||||
has_versions = True
|
||||
|
||||
labels = {
|
||||
'store_id': "Store ID",
|
||||
'vendor_id': "Vendor ID",
|
||||
'txn_id': "Transaction ID",
|
||||
'quickbooks_vendor_terms': "Vendor Terms",
|
||||
'quickbooks_export_template': "Export Template",
|
||||
'status_code': "Status",
|
||||
}
|
||||
|
||||
grid_columns = [
|
||||
'invoice_date',
|
||||
'invoice_number',
|
||||
'invoice_total',
|
||||
'store',
|
||||
'vendor',
|
||||
'quickbooks_vendor_terms',
|
||||
'quickbooks_export_template',
|
||||
'status_code',
|
||||
]
|
||||
|
||||
form_fields = [
|
||||
'store',
|
||||
'vendor',
|
||||
'txn_id',
|
||||
'invoice_number',
|
||||
'invoice_date',
|
||||
'invoice_total',
|
||||
'quickbooks_vendor_name',
|
||||
'quickbooks_vendor_terms',
|
||||
'quickbooks_vendor_bank_account',
|
||||
'quickbooks_export_template',
|
||||
'status_code',
|
||||
'status_text',
|
||||
'deleted',
|
||||
'deleted_by',
|
||||
'exported',
|
||||
'exported_by',
|
||||
]
|
||||
|
||||
has_rows = True
|
||||
model_row_class = QuickbooksExportableInvoiceDistribution
|
||||
rows_filterable = False
|
||||
rows_pageable = False
|
||||
rows_viewable = False
|
||||
|
||||
# TODO: this does not work right yet, e.g. clicking the View
|
||||
# action link will still trigger row-click event
|
||||
# clicking_row_checks_box = True
|
||||
|
||||
row_labels = {
|
||||
'department_id': "Department ID",
|
||||
'status_code': "Status",
|
||||
}
|
||||
|
||||
row_grid_columns = [
|
||||
'department_id',
|
||||
'department',
|
||||
'quickbooks_expense_account',
|
||||
'quickbooks_expense_class',
|
||||
'source_amount',
|
||||
'calculated_percent',
|
||||
'calculated_amount',
|
||||
'status_code',
|
||||
]
|
||||
|
||||
def configure_grid(self, g):
|
||||
super(ExportableInvoiceView, self).configure_grid(g)
|
||||
model = self.model
|
||||
|
||||
# store
|
||||
g.set_joiner('store', lambda q: q.outerjoin(model.Store))
|
||||
g.set_filter('store', model.Store.name)
|
||||
g.set_sorter('store', model.Store.name)
|
||||
|
||||
# vendor
|
||||
g.set_joiner('vendor', lambda q: q.outerjoin(model.Vendor))
|
||||
g.set_filter('vendor', model.Vendor.name)
|
||||
g.set_sorter('vendor', model.Vendor.name)
|
||||
g.set_link('vendor')
|
||||
|
||||
# invoice_number
|
||||
g.filters['invoice_number'].default_active = True
|
||||
g.filters['invoice_number'].default_verb = 'contains'
|
||||
g.set_link('invoice_number')
|
||||
|
||||
# invoice_date
|
||||
g.set_sort_defaults('invoice_date', 'desc')
|
||||
g.set_link('invoice_date')
|
||||
|
||||
# invoice_total
|
||||
g.set_type('invoice_total', 'currency')
|
||||
|
||||
# status_code
|
||||
g.set_enum('status_code', model.QuickbooksExportableInvoice.STATUS)
|
||||
g.set_renderer('status_code', self.make_status_renderer(
|
||||
model.QuickbooksExportableInvoice.STATUS))
|
||||
|
||||
# exported
|
||||
g.filters['exported'].default_active = True
|
||||
g.filters['exported'].default_verb = 'is_null'
|
||||
|
||||
# deleted
|
||||
g.filters['deleted'].default_active = True
|
||||
g.filters['deleted'].default_verb = 'is_null'
|
||||
|
||||
if self.has_perm('export'):
|
||||
g.checkboxes = True
|
||||
g.check_handler = 'rowChecked'
|
||||
g.check_all_handler = 'allChecked'
|
||||
|
||||
def grid_extra_class(self, invoice, i):
|
||||
if not self.exportable(invoice):
|
||||
if invoice.status_code in (invoice.STATUS_DEPTS_IGNORED,
|
||||
invoice.STATUS_EXPORTED,
|
||||
invoice.STATUS_DELETED):
|
||||
return 'notice'
|
||||
return 'warning'
|
||||
|
||||
def checkbox(self, invoice):
|
||||
return self.exportable(invoice)
|
||||
|
||||
def checked(self, invoice):
|
||||
return invoice.uuid in self.get_selected()
|
||||
|
||||
def template_kwargs_index(self, **kwargs):
|
||||
kwargs = super(ExportableInvoiceView, self).template_kwargs_index(**kwargs)
|
||||
kwargs['selected'] = self.get_selected()
|
||||
return kwargs
|
||||
|
||||
def get_selected(self):
|
||||
route_prefix = self.get_route_prefix()
|
||||
return self.request.session.get('{}.selected'.format(route_prefix), set())
|
||||
|
||||
def set_selected(self, selected):
|
||||
route_prefix = self.get_route_prefix()
|
||||
self.request.session['{}.selected'.format(route_prefix)] = selected
|
||||
|
||||
def exportable(self, invoice):
|
||||
"""
|
||||
Return boolean indicating whether the given invoice is exportable.
|
||||
"""
|
||||
return invoice.status_code == invoice.STATUS_EXPORTABLE
|
||||
|
||||
def configure_form(self, f):
|
||||
super(ExportableInvoiceView, self).configure_form(f)
|
||||
model = self.model
|
||||
invoice = f.model_instance
|
||||
|
||||
# store
|
||||
f.set_renderer('store', self.render_store)
|
||||
|
||||
# vendor
|
||||
f.set_renderer('vendor', self.render_vendor)
|
||||
|
||||
# invoice_total
|
||||
f.set_type('invoice_total', 'currency')
|
||||
|
||||
# status
|
||||
f.set_enum('status_code', model.QuickbooksExportableInvoice.STATUS)
|
||||
|
||||
# exported
|
||||
if self.creating or not invoice.exported:
|
||||
f.remove('exported', 'exported_by')
|
||||
|
||||
# deleted
|
||||
if self.creating or not invoice.deleted:
|
||||
f.remove('deleted', 'deleted_by')
|
||||
|
||||
def get_row_data(self, invoice):
|
||||
model = self.model
|
||||
return self.Session.query(model.QuickbooksExportableInvoiceDistribution)\
|
||||
.filter(model.QuickbooksExportableInvoiceDistribution.invoice == invoice)
|
||||
|
||||
def get_parent(self, dist):
|
||||
return dist.invoice
|
||||
|
||||
def configure_row_grid(self, g):
|
||||
super(ExportableInvoiceView, self).configure_row_grid(g)
|
||||
model = self.model
|
||||
|
||||
# department_id
|
||||
g.set_sort_defaults('department_id')
|
||||
|
||||
# amounts etc.
|
||||
g.set_type('source_amount', 'currency')
|
||||
g.set_type('calculated_percent', 'percent')
|
||||
g.set_type('calculated_amount', 'currency')
|
||||
|
||||
# status
|
||||
g.set_enum('status_code', model.QuickbooksExportableInvoiceDistribution.STATUS)
|
||||
|
||||
def row_grid_extra_class(self, dist, i):
|
||||
if dist.status_code in (dist.STATUS_DEPT_IGNORED,
|
||||
dist.STATUS_EXPORTED):
|
||||
return 'notice'
|
||||
elif dist.status_code != dist.STATUS_EXPORTABLE:
|
||||
return 'warning'
|
||||
|
||||
def refresh(self):
|
||||
"""
|
||||
View to refresh data for a single invoice.
|
||||
"""
|
||||
invoice = self.get_instance()
|
||||
self.refresh_invoice(invoice)
|
||||
return self.redirect(self.get_action_url('view', invoice))
|
||||
|
||||
def refresh_invoice(self, invoice):
|
||||
"""
|
||||
Logic to actually refresh data for the given invoice.
|
||||
Implement as needed.
|
||||
"""
|
||||
|
||||
def select(self):
|
||||
"""
|
||||
Mark one or more invoices as selected, within the current user's session.
|
||||
"""
|
||||
model = self.model
|
||||
|
||||
form = forms.Form(schema=ToggleInvoices(), request=self.request)
|
||||
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.query(model.QuickbooksExportableInvoice).get(uuid)
|
||||
if invoice and self.exportable(invoice):
|
||||
invoices.append(invoice)
|
||||
if not invoices:
|
||||
return {'error': "Must specify one or more valid invoice UUIDs."}
|
||||
|
||||
selected = self.get_selected()
|
||||
for invoice in invoices:
|
||||
selected.add(invoice.uuid)
|
||||
self.set_selected(selected)
|
||||
return {
|
||||
'ok': True,
|
||||
'selected_count': len(selected),
|
||||
}
|
||||
|
||||
def deselect(self):
|
||||
"""
|
||||
Mark one or more invoices as *not* selected, within the current user's session.
|
||||
"""
|
||||
model = self.model
|
||||
|
||||
form = forms.Form(schema=ToggleInvoices(), request=self.request)
|
||||
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.query(model.QuickbooksExportableInvoice).get(uuid)
|
||||
if invoice and self.exportable(invoice):
|
||||
invoices.append(invoice)
|
||||
if not invoices:
|
||||
return {'error': "Must specify one or more valid invoice UUIDs."}
|
||||
|
||||
selected = self.get_selected()
|
||||
for invoice in invoices:
|
||||
selected.discard(invoice.uuid)
|
||||
self.set_selected(selected)
|
||||
return {
|
||||
'ok': True,
|
||||
'selected_count': len(selected),
|
||||
}
|
||||
|
||||
def export(self):
|
||||
"""
|
||||
Export all currently-selected invoices.
|
||||
"""
|
||||
model = self.model
|
||||
|
||||
selected = self.get_selected()
|
||||
if not selected:
|
||||
self.request.session.flash("You must first select one or more "
|
||||
"invoices.", 'error')
|
||||
return self.redirect(self.get_index_url())
|
||||
|
||||
invoices = []
|
||||
for uuid in selected:
|
||||
invoice = self.Session.query(model.QuickbooksExportableInvoice).get(uuid)
|
||||
if invoice and invoice.status_code == invoice.STATUS_EXPORTABLE:
|
||||
invoices.append(invoice)
|
||||
else:
|
||||
log.warning("invoice not found or wrong status: %s", uuid)
|
||||
|
||||
if not invoices:
|
||||
self.request.session.flash("Hm, was unable to determine any invoices "
|
||||
"to export.", 'error')
|
||||
return self.redirect(self.get_index_url())
|
||||
|
||||
# perform actual export and capture result
|
||||
result = self.do_export(invoices)
|
||||
|
||||
# clear out current checkbox selection
|
||||
self.set_selected(set())
|
||||
|
||||
# result may be a redirect, e.g. to new export record. if so
|
||||
# then just return as-is
|
||||
if isinstance(result, HTTPFound):
|
||||
return result
|
||||
|
||||
# otherwise go back to invoice list
|
||||
return self.redirect(self.get_index_url())
|
||||
|
||||
def do_export(self, invoices):
|
||||
export = self.make_invoice_export(invoices)
|
||||
self.update_export_status(invoices)
|
||||
url = self.request.route_url('quickbooks.invoice_exports.view',
|
||||
uuid=export.uuid)
|
||||
return self.redirect(url)
|
||||
|
||||
def make_invoice_export_filename(self, invoices):
|
||||
raise NotImplementedError
|
||||
|
||||
def make_invoice_export(self, invoices):
|
||||
model = self.model
|
||||
|
||||
export = model.QuickbooksInvoiceExport()
|
||||
export.created_by = self.request.user
|
||||
export.record_count = len(invoices)
|
||||
export.filename = self.make_invoice_export_filename(invoices)
|
||||
self.Session.add(export)
|
||||
self.Session.flush()
|
||||
|
||||
path = export.filepath(self.rattail_config, filename=export.filename,
|
||||
makedirs=True)
|
||||
|
||||
try:
|
||||
self.write_invoice_export_file(export, path, invoices)
|
||||
|
||||
except Exception as error:
|
||||
log.warning("failed to write invoice export file for %s", export.uuid,
|
||||
exc_info=True)
|
||||
self.request.session.flash("Failed to write export file: {}".format(
|
||||
simple_error(error)), 'error')
|
||||
|
||||
return export
|
||||
|
||||
def write_invoice_export_file(self, export, path, invoices, progress=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def update_export_status(self, invoices):
|
||||
app = self.get_rattail_app()
|
||||
now = app.make_utc()
|
||||
for invoice in invoices:
|
||||
invoice.exported = now
|
||||
invoice.exported_by = self.request.user
|
||||
invoice.status_code = invoice.STATUS_EXPORTED
|
||||
for dist in invoice.distributions:
|
||||
if dist.status_code == dist.STATUS_EXPORTABLE:
|
||||
dist.status_code = dist.STATUS_EXPORTED
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
cls._invoice_defaults(config)
|
||||
cls._defaults(config)
|
||||
|
||||
@classmethod
|
||||
def _invoice_defaults(cls, config):
|
||||
route_prefix = cls.get_route_prefix()
|
||||
url_prefix = cls.get_url_prefix()
|
||||
instance_url_prefix = cls.get_instance_url_prefix()
|
||||
permission_prefix = cls.get_permission_prefix()
|
||||
model_title_plural = cls.get_model_title_plural()
|
||||
|
||||
# nb. must fix permission group title
|
||||
config.add_tailbone_permission_group(permission_prefix,
|
||||
model_title_plural, overwrite=False)
|
||||
|
||||
# select
|
||||
config.add_route('{}.select'.format(route_prefix),
|
||||
'{}/select'.format(url_prefix))
|
||||
config.add_view(cls, attr='select',
|
||||
route_name='{}.select'.format(route_prefix),
|
||||
request_method='POST',
|
||||
permission='{}.export'.format(permission_prefix),
|
||||
renderer='json')
|
||||
|
||||
# deselect
|
||||
config.add_route('{}.deselect'.format(route_prefix),
|
||||
'{}/deselect'.format(url_prefix))
|
||||
config.add_view(cls, attr='deselect',
|
||||
route_name='{}.deselect'.format(route_prefix),
|
||||
request_method='POST',
|
||||
permission='{}.export'.format(permission_prefix),
|
||||
renderer='json')
|
||||
|
||||
# refresh
|
||||
config.add_route('{}.refresh'.format(route_prefix),
|
||||
'{}/refresh'.format(instance_url_prefix))
|
||||
config.add_view(cls, attr='refresh',
|
||||
route_name='{}.refresh'.format(route_prefix),
|
||||
request_method='POST',
|
||||
permission='{}.export'.format(permission_prefix))
|
||||
|
||||
# export
|
||||
config.add_tailbone_permission(permission_prefix,
|
||||
'{}.export'.format(permission_prefix),
|
||||
"Export Invoices")
|
||||
config.add_route('{}.export'.format(route_prefix),
|
||||
'{}/export'.format(url_prefix))
|
||||
config.add_view(cls, attr='export',
|
||||
route_name='{}.export'.format(route_prefix),
|
||||
request_method='POST',
|
||||
permission='{}.export'.format(permission_prefix))
|
||||
|
||||
|
||||
class InvoiceExportView(ExportMasterView):
|
||||
"""
|
||||
Master view for Quickbooks invoice exports.
|
||||
"""
|
||||
model_class = QuickbooksInvoiceExport
|
||||
route_prefix = 'quickbooks.invoice_exports'
|
||||
url_prefix = '/quickbooks/exports/invoice'
|
||||
downloadable = True
|
||||
delete_export_files = True
|
||||
|
||||
grid_columns = [
|
||||
'id',
|
||||
'created',
|
||||
'created_by',
|
||||
'filename',
|
||||
'record_count',
|
||||
]
|
||||
|
||||
form_fields = [
|
||||
'id',
|
||||
'created',
|
||||
'created_by',
|
||||
'record_count',
|
||||
'filename',
|
||||
]
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
ExportableInvoiceView = kwargs.get('ExportableInvoiceView', base['ExportableInvoiceView'])
|
||||
ExportableInvoiceView.defaults(config)
|
||||
|
||||
InvoiceExportView = kwargs.get('InvoiceExportView', base['InvoiceExportView'])
|
||||
InvoiceExportView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
defaults(config)
|
53
tailbone_quickbooks/views/stores.py
Normal file
53
tailbone_quickbooks/views/stores.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Store views, w/ Quickbooks integration
|
||||
"""
|
||||
|
||||
from tailbone.views import ViewSupplement
|
||||
|
||||
|
||||
class StoreViewSupplement(ViewSupplement):
|
||||
"""
|
||||
Store view supplement for Quickbooks integration
|
||||
"""
|
||||
route_prefix = 'stores'
|
||||
|
||||
def get_grid_query(self, query):
|
||||
model = self.model
|
||||
return query.outerjoin(model.QuickbooksStore)
|
||||
|
||||
def configure_grid(self, g):
|
||||
model = self.model
|
||||
g.set_filter('quickbooks_location', model.QuickbooksStore.quickbooks_location)
|
||||
|
||||
def configure_form(self, f):
|
||||
f.append('quickbooks_location')
|
||||
|
||||
def get_version_child_classes(self):
|
||||
model = self.model
|
||||
return [model.QuickbooksStore]
|
||||
|
||||
|
||||
def includeme(config):
|
||||
StoreViewSupplement.defaults(config)
|
57
tailbone_quickbooks/views/vendors.py
Normal file
57
tailbone_quickbooks/views/vendors.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Vendor views, w/ Quickbooks integration
|
||||
"""
|
||||
|
||||
from tailbone.views import ViewSupplement
|
||||
|
||||
|
||||
class VendorViewSupplement(ViewSupplement):
|
||||
"""
|
||||
Vendor view supplement for Quickbooks integration
|
||||
"""
|
||||
route_prefix = 'vendors'
|
||||
|
||||
def get_grid_query(self, query):
|
||||
model = self.model
|
||||
return query.outerjoin(model.QuickbooksVendor)
|
||||
|
||||
def configure_grid(self, g):
|
||||
model = self.model
|
||||
g.set_filter('quickbooks_name', model.QuickbooksVendor.quickbooks_name)
|
||||
g.set_filter('quickbooks_bank_account', model.QuickbooksVendor.quickbooks_bank_account)
|
||||
g.set_filter('quickbooks_terms', model.QuickbooksVendor.quickbooks_terms)
|
||||
|
||||
def configure_form(self, f):
|
||||
f.append('quickbooks_name')
|
||||
f.append('quickbooks_bank_account')
|
||||
f.append('quickbooks_terms')
|
||||
|
||||
def get_version_child_classes(self):
|
||||
model = self.model
|
||||
return [model.QuickbooksVendor]
|
||||
|
||||
|
||||
def includeme(config):
|
||||
VendorViewSupplement.defaults(config)
|
47
tasks.py
Normal file
47
tasks.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
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
|
||||
c.run('rm -rf tailbone_quickbooks.egg-info')
|
||||
c.run('python setup.py sdist --formats=gztar')
|
||||
|
||||
# upload to PyPI
|
||||
filename = 'tailbone-quickbooks-{}.tar.gz'.format(__version__)
|
||||
c.run('twine upload dist/{}'.format(filename))
|
Loading…
Reference in a new issue