Add buefy theme support for ordering worksheet

This commit is contained in:
Lance Edgar 2020-08-20 17:56:19 -05:00
parent 7a01cb8873
commit 58362ae858
7 changed files with 282 additions and 84 deletions

View file

@ -4,6 +4,8 @@ const NumericInput = {
'<b-input', '<b-input',
':name="name"', ':name="name"',
':value="value"', ':value="value"',
'@focus="focus"',
'@blur="blur"',
'@keydown.native="keyDown"', '@keydown.native="keyDown"',
'@input="valueChanged"', '@input="valueChanged"',
'>', '>',
@ -18,6 +20,14 @@ const NumericInput = {
methods: { methods: {
focus(event) {
this.$emit('focus', event)
},
blur(event) {
this.$emit('blur', event)
},
keyDown(event) { keyDown(event) {
// by default we only allow numeric keys, and general navigation // by default we only allow numeric keys, and general navigation
// keys, but we might also allow Enter key // keys, but we might also allow Enter key

View file

@ -55,8 +55,15 @@
</%def> </%def>
<%def name="leading_buttons()"> <%def name="leading_buttons()">
% if master.has_worksheet and master.allow_worksheet(batch) and request.has_perm('{}.worksheet'.format(permission_prefix)): % if master.has_worksheet and master.allow_worksheet(batch) and master.has_perm('worksheet'):
<button type="button" class="load-worksheet">Edit as Worksheet</button> % if use_buefy:
<once-button tag="a"
href="${url('{}.worksheet'.format(route_prefix), uuid=batch.uuid)}"
text="Edit as Worksheet">
</once-button>
% else:
<button type="button" class="load-worksheet">Edit as Worksheet</button>
% endif
% endif % endif
</%def> </%def>

View file

@ -1,8 +1,9 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%inherit file="/base.mako" /> <%inherit file="/page.mako" />
<%def name="extra_javascript()"> <%def name="extra_javascript()">
${parent.extra_javascript()} ${parent.extra_javascript()}
% if not use_buefy:
<script type="text/javascript"> <script type="text/javascript">
$(function() { $(function() {
@ -17,6 +18,7 @@
}); });
</script> </script>
% endif
</%def> </%def>
<%def name="extra_styles()"> <%def name="extra_styles()">
@ -41,5 +43,9 @@
<%def name="worksheet_grid()"></%def> <%def name="worksheet_grid()"></%def>
<%def name="page_content()">
${self.worksheet_grid()}
</%def>
${self.worksheet_grid()}
${parent.body()}

View file

@ -1,10 +1,11 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%inherit file="/base.mako" /> <%inherit file="/batch/worksheet.mako" />
<%def name="title()">Ordering Worksheet</%def> <%def name="title()">Ordering Worksheet</%def>
<%def name="extra_javascript()"> <%def name="extra_javascript()">
${parent.extra_javascript()} ${parent.extra_javascript()}
% if not use_buefy:
${h.javascript_link(request.static_url('tailbone:static/js/numeric.js'))} ${h.javascript_link(request.static_url('tailbone:static/js/numeric.js'))}
<script type="text/javascript"> <script type="text/javascript">
@ -56,6 +57,7 @@
}); });
</script> </script>
% endif
</%def> </%def>
<%def name="extra_styles()"> <%def name="extra_styles()">
@ -112,71 +114,10 @@
</style> </style>
</%def> </%def>
<%def name="context_menu_items()"> <%def name="context_menu_items()">
<li>${h.link_to("Back to {}".format(model_title), url('ordering.view', uuid=batch.uuid))}</li> <li>${h.link_to("Back to {}".format(model_title), url('ordering.view', uuid=batch.uuid))}</li>
</%def> </%def>
##############################
## page body
##############################
<ul id="context-menu">
${self.context_menu_items()}
</ul>
<div class="form-wrapper">
<div class="field-wrapper">
<label>Vendor</label>
<div class="field">${h.link_to(vendor, url('vendors.view', uuid=vendor.uuid))}</div>
</div>
<div class="field-wrapper">
<label>Vendor Email</label>
<div class="field">${vendor.email or ''}</div>
</div>
<div class="field-wrapper">
<label>Vendor Fax</label>
<div class="field">${vendor.fax_number or ''}</div>
</div>
<div class="field-wrapper">
<label>Vendor Contact</label>
<div class="field">${vendor.contact or ''}</div>
</div>
<div class="field-wrapper">
<label>Vendor Phone</label>
<div class="field">${vendor.phone or ''}</div>
</div>
${self.extra_vendor_fields()}
<div class="field-wrapper po-total">
<label>PO Total</label>
## TODO: should not fall back to po_total
<div class="field">$${'{:0,.2f}'.format(batch.po_total_calculated or batch.po_total or 0)}</div>
</div>
</div><!-- form-wrapper -->
${self.order_form_grid()}
${h.form(url('ordering.worksheet_update', uuid=batch.uuid), id='item-update-form', style='display: none;')}
${h.csrf_token(request)}
${h.hidden('product_uuid')}
${h.hidden('cases_ordered')}
${h.hidden('units_ordered')}
${h.end_form()}
##############################
## methods
##############################
<%def name="extra_vendor_fields()"></%def> <%def name="extra_vendor_fields()"></%def>
<%def name="extra_count()">0</%def> <%def name="extra_count()">0</%def>
@ -189,7 +130,7 @@ ${h.end_form()}
<div class="grid"> <div class="grid">
<table class="order-form"> <table class="order-form">
<% column_count = 8 + len(header_columns) + (0 if ignore_cases else 1) + int(capture(self.extra_count)) %> <% column_count = 8 + len(header_columns) + (0 if ignore_cases else 1) + int(capture(self.extra_count)) %>
% for department in sorted(departments.values(), key=lambda d: d.name if d else ''): % for department in sorted(six.itervalues(departments), key=lambda d: d.name if d else ''):
<thead> <thead>
<tr> <tr>
<th class="department" colspan="${column_count}">Department <th class="department" colspan="${column_count}">Department
@ -200,7 +141,7 @@ ${h.end_form()}
% endif % endif
</th> </th>
</tr> </tr>
% for subdepartment in sorted(department._order_subdepartments.values(), key=lambda s: s.name if s else ''): % for subdepartment in sorted(six.itervalues(department._order_subdepartments), key=lambda s: s.name if s else ''):
<tr> <tr>
<th class="subdepartment" colspan="${column_count}">Subdepartment <th class="subdepartment" colspan="${column_count}">Subdepartment
% if subdepartment.number or subdepartment.name: % if subdepartment.number or subdepartment.name:
@ -245,7 +186,11 @@ ${h.end_form()}
</thead> </thead>
<tbody> <tbody>
% for i, cost in enumerate(subdepartment._order_costs, 1): % for i, cost in enumerate(subdepartment._order_costs, 1):
<tr data-uuid="${cost.product_uuid}" class="${'even' if i % 2 == 0 else 'odd'}"> <tr data-uuid="${cost.product_uuid}" class="${'even' if i % 2 == 0 else 'odd'}"
% if use_buefy:
:class="{active: activeUUID == '${cost.uuid}'}"
% endif
>
${self.order_form_row(cost)} ${self.order_form_row(cost)}
% for data in history: % for data in history:
<td class="scratch_pad"> <td class="scratch_pad">
@ -271,14 +216,34 @@ ${h.end_form()}
% endfor % endfor
% if not ignore_cases: % if not ignore_cases:
<td class="current-order"> <td class="current-order">
${h.text('cases_ordered_{}'.format(cost.uuid), value=int(cost._batchrow.cases_ordered or 0) if cost._batchrow else None)} % if use_buefy:
<numeric-input v-model="worksheet.cost_${cost.uuid}_cases"
@focus="activeUUID = '${cost.uuid}'; $event.target.select()"
@blur="activeUUID = null"
@keydown.native="inputKeydown($event, '${cost.uuid}', '${cost.product_uuid}')">
</numeric-input>
% else:
${h.text('cases_ordered_{}'.format(cost.uuid), value=int(cost._batchrow.cases_ordered or 0) if cost._batchrow else None)}
% endif
</td> </td>
% endif % endif
<td class="current-order"> <td class="current-order">
${h.text('units_ordered_{}'.format(cost.uuid), value=int(cost._batchrow.units_ordered or 0) if cost._batchrow else None)} % if use_buefy:
<numeric-input v-model="worksheet.cost_${cost.uuid}_units"
@focus="activeUUID = '${cost.uuid}'; $event.target.select()"
@blur="activeUUID = null"
@keydown.native="inputKeydown($event, '${cost.uuid}', '${cost.product_uuid}')">
</numeric-input>
% else:
${h.text('units_ordered_{}'.format(cost.uuid), value=int(cost._batchrow.units_ordered or 0) if cost._batchrow else None)}
% endif
</td> </td>
## TODO: should not fall back to po_total ## TODO: should not fall back to po_total
<td class="po-total">${'${:0,.2f}'.format(cost._batchrow.po_total_calculated or cost._batchrow.po_total or 0) if cost._batchrow else ''}</td> % if use_buefy:
<td class="po-total">{{ worksheet.cost_${cost.uuid}_total_display }}</td>
% else:
<td class="po-total">${'${:0,.2f}'.format(cost._batchrow.po_total_calculated or cost._batchrow.po_total or 0) if cost._batchrow else ''}</td>
% endif
${self.extra_td(cost)} ${self.extra_td(cost)}
</tr> </tr>
% endfor % endfor
@ -302,3 +267,166 @@ ${h.end_form()}
% endif % endif
</td> </td>
</%def> </%def>
<%def name="page_content()">
% if use_buefy:
<ordering-worksheet></ordering-worksheet>
% else:
<div class="form-wrapper">
<div class="field-wrapper">
<label>Vendor</label>
<div class="field">${h.link_to(vendor, url('vendors.view', uuid=vendor.uuid))}</div>
</div>
<div class="field-wrapper">
<label>Vendor Email</label>
<div class="field">${vendor.email or ''}</div>
</div>
<div class="field-wrapper">
<label>Vendor Fax</label>
<div class="field">${vendor.fax_number or ''}</div>
</div>
<div class="field-wrapper">
<label>Vendor Contact</label>
<div class="field">${vendor.contact or ''}</div>
</div>
<div class="field-wrapper">
<label>Vendor Phone</label>
<div class="field">${vendor.phone or ''}</div>
</div>
${self.extra_vendor_fields()}
<div class="field-wrapper po-total">
<label>PO Total</label>
## TODO: should not fall back to po_total
<div class="field">$${'{:0,.2f}'.format(batch.po_total_calculated or batch.po_total or 0)}</div>
</div>
</div><!-- form-wrapper -->
${self.order_form_grid()}
${h.form(url('ordering.worksheet_update', uuid=batch.uuid), id='item-update-form', style='display: none;')}
${h.csrf_token(request)}
${h.hidden('product_uuid')}
${h.hidden('cases_ordered')}
${h.hidden('units_ordered')}
${h.end_form()}
% endif
</%def>
<%def name="render_this_page_template()">
${parent.render_this_page_template()}
<script type="text/x-template" id="ordering-worksheet-template">
<div>
<div class="form-wrapper">
<div class="form">
<b-field horizontal label="Vendor">
${h.link_to(vendor, url('vendors.view', uuid=vendor.uuid))}
</b-field>
<b-field horizontal label="Vendor Email">
<span>${vendor.email or ''}</span>
</b-field>
<b-field horizontal label="Vendor Fax">
<span>${vendor.fax_number or ''}</span>
</b-field>
<b-field horizontal label="Vendor Contact">
<span>${vendor.contact or ''}</span>
</b-field>
<b-field horizontal label="Vendor Phone">
<span>${vendor.phone or ''}</span>
</b-field>
${self.extra_vendor_fields()}
<b-field horizontal label="PO Total">
<span>{{ poTotalDisplay }}</span>
</b-field>
</div> <!-- form -->
</div><!-- form-wrapper -->
${self.order_form_grid()}
</div>
</script>
</%def>
<%def name="make_this_page_component()">
${parent.make_this_page_component()}
<script type="text/javascript">
const OrderingWorksheet = {
template: '#ordering-worksheet-template',
data() {
return {
worksheet: ${json.dumps(worksheet_data)|n},
activeUUID: null,
poTotalDisplay: "$${'{:0,.2f}'.format(batch.po_total_calculated or batch.po_total or 0)}",
submitting: false,
## TODO: should find a better way to handle CSRF token
csrftoken: ${json.dumps(request.session.get_csrf_token() or request.session.new_csrf_token())|n},
}
},
methods: {
inputKeydown(event, cost_uuid, product_uuid) {
if (event.which == 13) {
if (!this.submitting) {
this.submitting = true
let url = '${url('ordering.worksheet_update', uuid=batch.uuid)}'
let params = {
product_uuid: product_uuid,
% if not ignore_cases:
cases_ordered: this.worksheet['cost_' + cost_uuid + '_cases'],
% endif
units_ordered: this.worksheet['cost_' + cost_uuid + '_units'],
}
let headers = {
## TODO: should find a better way to handle CSRF token
'X-CSRF-TOKEN': this.csrftoken,
}
## TODO: should find a better way to handle CSRF token
this.$http.post(url, params, {headers: headers}).then(response => {
if (response.data.error) {
alert(response.data.error)
} else {
this.worksheet['cost_' + cost_uuid + '_cases'] = response.data.row_cases_ordered
this.worksheet['cost_' + cost_uuid + '_units'] = response.data.row_units_ordered
this.worksheet['cost_' + cost_uuid + '_total_display'] = response.data.row_po_total_display
this.poTotalDisplay = response.data.batch_po_total_display
}
this.submitting = false
})
}
}
},
},
}
Vue.component('ordering-worksheet', OrderingWorksheet)
</script>
</%def>
##############################
## page body
##############################
${parent.body()}

View file

@ -1514,7 +1514,8 @@ class BatchMasterView(MasterView):
config.add_route('{}.worksheet'.format(route_prefix), '{}/{{{}}}/worksheet'.format(url_prefix, model_key)) config.add_route('{}.worksheet'.format(route_prefix), '{}/{{{}}}/worksheet'.format(url_prefix, model_key))
config.add_view(cls, attr='worksheet', route_name='{}.worksheet'.format(route_prefix), config.add_view(cls, attr='worksheet', route_name='{}.worksheet'.format(route_prefix),
permission='{}.worksheet'.format(permission_prefix)) permission='{}.worksheet'.format(permission_prefix))
config.add_route('{}.worksheet_update'.format(route_prefix), '{}/{{{}}}/worksheet/update'.format(url_prefix, model_key)) config.add_route('{}.worksheet_update'.format(route_prefix), '{}/{{{}}}/worksheet/update'.format(url_prefix, model_key),
request_method='POST')
config.add_view(cls, attr='worksheet_update', route_name='{}.worksheet_update'.format(route_prefix), config.add_view(cls, attr='worksheet_update', route_name='{}.worksheet_update'.format(route_prefix),
renderer='json', permission='{}.worksheet'.format(permission_prefix)) renderer='json', permission='{}.worksheet'.format(permission_prefix))

View file

@ -328,8 +328,9 @@ class PurchasingBatchView(BatchMasterView):
buyer_display = six.text_type(buyer) buyer_display = six.text_type(buyer)
elif self.creating: elif self.creating:
buyer = self.request.user.employee buyer = self.request.user.employee
buyer_display = six.text_type(buyer) if buyer:
f.set_default('buyer_uuid', buyer.uuid) buyer_display = six.text_type(buyer)
f.set_default('buyer_uuid', buyer.uuid)
elif self.editing: elif self.editing:
buyer_display = six.text_type(batch.buyer or '') buyer_display = six.text_type(batch.buyer or '')
buyers_url = self.request.route_url('employees.autocomplete') buyers_url = self.request.route_url('employees.autocomplete')

View file

@ -27,6 +27,7 @@ Views for 'ordering' (purchasing) batches
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
import os import os
import json
import six import six
import openpyxl import openpyxl
@ -245,6 +246,11 @@ class OrderingBatchView(PurchasingBatchView):
order_date = batch.date_ordered order_date = batch.date_ordered
if not order_date: if not order_date:
order_date = localtime(self.rattail_config).date() order_date = localtime(self.rattail_config).date()
buefy_data = None
if self.get_use_buefy():
buefy_data = self.get_worksheet_buefy_data(departments)
return self.render_to_response('worksheet', { return self.render_to_response('worksheet', {
'batch': batch, 'batch': batch,
'order_date': order_date, 'order_date': order_date,
@ -257,8 +263,32 @@ class OrderingBatchView(PurchasingBatchView):
'get_upc': lambda p: p.upc.pretty() if p.upc else '', 'get_upc': lambda p: p.upc.pretty() if p.upc else '',
'header_columns': self.order_form_header_columns, 'header_columns': self.order_form_header_columns,
'ignore_cases': not self.handler.allow_cases(), 'ignore_cases': not self.handler.allow_cases(),
'worksheet_data': buefy_data,
}) })
def get_worksheet_buefy_data(self, departments):
data = {}
for department in six.itervalues(departments):
for subdepartment in six.itervalues(department._order_subdepartments):
for i, cost in enumerate(subdepartment._order_costs, 1):
cases = int(cost._batchrow.cases_ordered or 0) if cost._batchrow else None
units = int(cost._batchrow.units_ordered or 0) if cost._batchrow else None
key = 'cost_{}'.format(cost.uuid)
data['{}_cases'.format(key)] = cases
data['{}_units'.format(key)] = units
total = 0
row = cost._batchrow
if row:
total = row.po_total_calculated or row.po_total or 0
if not (total or cases or units):
display = ''
else:
display = '${:0,.2f}'.format(total)
data['{}_total_display'.format(key)] = display
return data
def worksheet_update(self): def worksheet_update(self):
""" """
Handles AJAX requests to update the order quantities for some row Handles AJAX requests to update the order quantities for some row
@ -282,21 +312,34 @@ class OrderingBatchView(PurchasingBatchView):
""" """
batch = self.get_instance() batch = self.get_instance()
cases_ordered = self.request.POST.get('cases_ordered', '0') try:
if not cases_ordered or not cases_ordered.isdigit(): data = self.request.json_body
return {'error': "Invalid value for cases ordered: {}".format(cases_ordered)} except json.JSONDecodeError:
cases_ordered = int(cases_ordered) data = self.request.POST
cases_ordered = data.get('cases_ordered')
if cases_ordered is None:
cases_ordered = 0
elif not isinstance(cases_ordered, int):
if cases_ordered == '':
cases_ordered = 0
else:
cases_ordered = int(cases_ordered)
if cases_ordered >= 100000: # TODO: really this depends on underlying column if cases_ordered >= 100000: # TODO: really this depends on underlying column
return {'error': "Invalid value for cases ordered: {}".format(cases_ordered)} return {'error': "Invalid value for cases ordered: {}".format(cases_ordered)}
units_ordered = self.request.POST.get('units_ordered', '0') units_ordered = data.get('units_ordered')
if not units_ordered or not units_ordered.isdigit(): if units_ordered is None:
return {'error': "Invalid value for units ordered: {}".format(units_ordered)} units_ordered = 0
units_ordered = int(units_ordered) elif not isinstance(units_ordered, int):
if units_ordered == '':
units_ordered = 0
else:
units_ordered = int(units_ordered)
if units_ordered >= 100000: # TODO: really this depends on underlying column if units_ordered >= 100000: # TODO: really this depends on underlying column
return {'error': "Invalid value for units ordered: {}".format(units_ordered)} return {'error': "Invalid value for units ordered: {}".format(units_ordered)}
uuid = self.request.POST.get('product_uuid') uuid = data.get('product_uuid')
product = self.Session.query(model.Product).get(uuid) if uuid else None product = self.Session.query(model.Product).get(uuid) if uuid else None
if not product: if not product:
return {'error': "Product not found"} return {'error': "Product not found"}
@ -336,8 +379,10 @@ class OrderingBatchView(PurchasingBatchView):
'row_units_ordered': int(row.units_ordered or 0) if row else None, 'row_units_ordered': int(row.units_ordered or 0) if row else None,
'row_po_total': '${:0,.2f}'.format(row.po_total or 0) if row else None, 'row_po_total': '${:0,.2f}'.format(row.po_total or 0) if row else None,
'row_po_total_calculated': '${:0,.2f}'.format(row.po_total_calculated or 0) if row else None, 'row_po_total_calculated': '${:0,.2f}'.format(row.po_total_calculated or 0) if row else None,
'row_po_total_display': '${:0,.2f}'.format(row.po_total_calculated or row.po_total or 0) if row else None,
'batch_po_total': '${:0,.2f}'.format(batch.po_total or 0), 'batch_po_total': '${:0,.2f}'.format(batch.po_total or 0),
'batch_po_total_calculated': '${:0,.2f}'.format(batch.po_total_calculated or 0), 'batch_po_total_calculated': '${:0,.2f}'.format(batch.po_total_calculated or 0),
'batch_po_total_display': '${:0,.2f}'.format(batch.po_total_calculated or batch.po_total or 0),
} }
def render_mobile_listitem(self, batch, i): def render_mobile_listitem(self, batch, i):