Add support for Vendor -> Quickbooks Bank Accounts field

this feels a bit hacky yet, but had to introduce some new mechanisms
to allow for extra template stuff while avoiding adding a new
vendors/edit (etc.) template in this project, since we are using a
view supplement..ugh
This commit is contained in:
Lance Edgar 2024-04-16 18:23:49 -05:00
parent ddeb4545a6
commit 7e08dd0f89
3 changed files with 277 additions and 1 deletions

View file

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

View file

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

View file

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