Initial views, and basic invoice export UI tools
This commit is contained in:
		
						commit
						7384bd1fea
					
				
					 17 changed files with 1029 additions and 0 deletions
				
			
		
							
								
								
									
										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)
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue