Add initial views for work orders
at least a head start maybe
This commit is contained in:
parent
a6d5b262f9
commit
ca5e2c1ff9
238
tailbone/api/workorders.py
Normal file
238
tailbone/api/workorders.py
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
# -*- 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 Web API - Work Order Views
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from rattail.db.model import WorkOrder
|
||||||
|
from rattail.time import localtime
|
||||||
|
from rattail.util import OrderedDict
|
||||||
|
|
||||||
|
from cornice import Service
|
||||||
|
|
||||||
|
from tailbone.api import APIMasterView2 as APIMasterView
|
||||||
|
|
||||||
|
|
||||||
|
class WorkOrderView(APIMasterView):
|
||||||
|
|
||||||
|
model_class = WorkOrder
|
||||||
|
collection_url_prefix = '/workorders'
|
||||||
|
object_url_prefix = '/workorder'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(WorkOrderView, self).__init__(*args, **kwargs)
|
||||||
|
app = self.get_rattail_app()
|
||||||
|
self.workorder_handler = app.get_workorder_handler()
|
||||||
|
|
||||||
|
def normalize(self, workorder):
|
||||||
|
return {
|
||||||
|
'_str': six.text_type(workorder),
|
||||||
|
'uuid': workorder.uuid,
|
||||||
|
'id': workorder.id,
|
||||||
|
'customer_uuid': workorder.customer.uuid,
|
||||||
|
'customer_name': workorder.customer.name,
|
||||||
|
'notes': workorder.notes,
|
||||||
|
'status_code': workorder.status_code,
|
||||||
|
'status_label': self.enum.WORKORDER_STATUS[workorder.status_code],
|
||||||
|
'date_submitted': six.text_type(workorder.date_submitted or ''),
|
||||||
|
'date_received': six.text_type(workorder.date_received or ''),
|
||||||
|
'date_released': six.text_type(workorder.date_released or ''),
|
||||||
|
'date_delivered': six.text_type(workorder.date_delivered or ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
def update_object(self, workorder, data):
|
||||||
|
date_fields = [
|
||||||
|
'date_submitted',
|
||||||
|
'date_received',
|
||||||
|
'date_released',
|
||||||
|
'date_delivered',
|
||||||
|
]
|
||||||
|
|
||||||
|
# coerce date field values to proper datetime.date objects
|
||||||
|
for field in date_fields:
|
||||||
|
if field in data:
|
||||||
|
if data[field] == '':
|
||||||
|
data[field] = None
|
||||||
|
else:
|
||||||
|
date = datetime.datetime.strptime(data[field], '%Y-%m-%d').date()
|
||||||
|
data[field] = date
|
||||||
|
|
||||||
|
# coerce status code value to proper integer
|
||||||
|
if 'status_code' in data:
|
||||||
|
data['status_code'] = int(data['status_code'])
|
||||||
|
|
||||||
|
return super(WorkOrderView, self).update_object(workorder, data)
|
||||||
|
|
||||||
|
def status_codes(self):
|
||||||
|
"""
|
||||||
|
Retrieve all info about possible work order status codes.
|
||||||
|
"""
|
||||||
|
return self.workorder_handler.status_codes()
|
||||||
|
|
||||||
|
def receive(self):
|
||||||
|
"""
|
||||||
|
Sets work order status to "received".
|
||||||
|
"""
|
||||||
|
workorder = self.get_object()
|
||||||
|
self.workorder_handler.receive(workorder)
|
||||||
|
self.Session.flush()
|
||||||
|
return self.normalize(workorder)
|
||||||
|
|
||||||
|
def await_estimate(self):
|
||||||
|
"""
|
||||||
|
Sets work order status to "awaiting estimate confirmation".
|
||||||
|
"""
|
||||||
|
workorder = self.get_object()
|
||||||
|
self.workorder_handler.await_estimate(workorder)
|
||||||
|
self.Session.flush()
|
||||||
|
return self.normalize(workorder)
|
||||||
|
|
||||||
|
def await_parts(self):
|
||||||
|
"""
|
||||||
|
Sets work order status to "awaiting parts".
|
||||||
|
"""
|
||||||
|
workorder = self.get_object()
|
||||||
|
self.workorder_handler.await_parts(workorder)
|
||||||
|
self.Session.flush()
|
||||||
|
return self.normalize(workorder)
|
||||||
|
|
||||||
|
def work_on_it(self):
|
||||||
|
"""
|
||||||
|
Sets work order status to "working on it".
|
||||||
|
"""
|
||||||
|
workorder = self.get_object()
|
||||||
|
self.workorder_handler.work_on_it(workorder)
|
||||||
|
self.Session.flush()
|
||||||
|
return self.normalize(workorder)
|
||||||
|
|
||||||
|
def release(self):
|
||||||
|
"""
|
||||||
|
Sets work order status to "released".
|
||||||
|
"""
|
||||||
|
workorder = self.get_object()
|
||||||
|
self.workorder_handler.release(workorder)
|
||||||
|
self.Session.flush()
|
||||||
|
return self.normalize(workorder)
|
||||||
|
|
||||||
|
def deliver(self):
|
||||||
|
"""
|
||||||
|
Sets work order status to "delivered".
|
||||||
|
"""
|
||||||
|
workorder = self.get_object()
|
||||||
|
self.workorder_handler.deliver(workorder)
|
||||||
|
self.Session.flush()
|
||||||
|
return self.normalize(workorder)
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
"""
|
||||||
|
Sets work order status to "canceled".
|
||||||
|
"""
|
||||||
|
workorder = self.get_object()
|
||||||
|
self.workorder_handler.cancel(workorder)
|
||||||
|
self.Session.flush()
|
||||||
|
return self.normalize(workorder)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def defaults(cls, config):
|
||||||
|
cls._defaults(config)
|
||||||
|
cls._workorder_defaults(config)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _workorder_defaults(cls, config):
|
||||||
|
route_prefix = cls.get_route_prefix()
|
||||||
|
permission_prefix = cls.get_permission_prefix()
|
||||||
|
collection_url_prefix = cls.get_collection_url_prefix()
|
||||||
|
object_url_prefix = cls.get_object_url_prefix()
|
||||||
|
|
||||||
|
# status codes
|
||||||
|
status_codes = Service(name='{}.status_codes'.format(route_prefix),
|
||||||
|
path='{}/status-codes'.format(collection_url_prefix))
|
||||||
|
status_codes.add_view('GET', 'status_codes', klass=cls,
|
||||||
|
permission='{}.list'.format(permission_prefix))
|
||||||
|
config.add_cornice_service(status_codes)
|
||||||
|
|
||||||
|
# receive
|
||||||
|
receive = Service(name='{}.receive'.format(route_prefix),
|
||||||
|
path='{}/{{uuid}}/receive'.format(object_url_prefix))
|
||||||
|
receive.add_view('POST', 'receive', klass=cls,
|
||||||
|
permission='{}.edit'.format(permission_prefix))
|
||||||
|
config.add_cornice_service(receive)
|
||||||
|
|
||||||
|
# await estimate confirmation
|
||||||
|
await_estimate = Service(name='{}.await_estimate'.format(route_prefix),
|
||||||
|
path='{}/{{uuid}}/await-estimate'.format(object_url_prefix))
|
||||||
|
await_estimate.add_view('POST', 'await_estimate', klass=cls,
|
||||||
|
permission='{}.edit'.format(permission_prefix))
|
||||||
|
config.add_cornice_service(await_estimate)
|
||||||
|
|
||||||
|
# await parts
|
||||||
|
await_parts = Service(name='{}.await_parts'.format(route_prefix),
|
||||||
|
path='{}/{{uuid}}/await-parts'.format(object_url_prefix))
|
||||||
|
await_parts.add_view('POST', 'await_parts', klass=cls,
|
||||||
|
permission='{}.edit'.format(permission_prefix))
|
||||||
|
config.add_cornice_service(await_parts)
|
||||||
|
|
||||||
|
# work on it
|
||||||
|
work_on_it = Service(name='{}.work_on_it'.format(route_prefix),
|
||||||
|
path='{}/{{uuid}}/work-on-it'.format(object_url_prefix))
|
||||||
|
work_on_it.add_view('POST', 'work_on_it', klass=cls,
|
||||||
|
permission='{}.edit'.format(permission_prefix))
|
||||||
|
config.add_cornice_service(work_on_it)
|
||||||
|
|
||||||
|
# release
|
||||||
|
release = Service(name='{}.release'.format(route_prefix),
|
||||||
|
path='{}/{{uuid}}/release'.format(object_url_prefix))
|
||||||
|
release.add_view('POST', 'release', klass=cls,
|
||||||
|
permission='{}.edit'.format(permission_prefix))
|
||||||
|
config.add_cornice_service(release)
|
||||||
|
|
||||||
|
# deliver
|
||||||
|
deliver = Service(name='{}.deliver'.format(route_prefix),
|
||||||
|
path='{}/{{uuid}}/deliver'.format(object_url_prefix))
|
||||||
|
deliver.add_view('POST', 'deliver', klass=cls,
|
||||||
|
permission='{}.edit'.format(permission_prefix))
|
||||||
|
config.add_cornice_service(deliver)
|
||||||
|
|
||||||
|
# cancel
|
||||||
|
cancel = Service(name='{}.cancel'.format(route_prefix),
|
||||||
|
path='{}/{{uuid}}/cancel'.format(object_url_prefix))
|
||||||
|
cancel.add_view('POST', 'cancel', klass=cls,
|
||||||
|
permission='{}.edit'.format(permission_prefix))
|
||||||
|
config.add_cornice_service(cancel)
|
||||||
|
|
||||||
|
|
||||||
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
WorkOrderView = kwargs.get('WorkOrderView', base['WorkOrderView'])
|
||||||
|
WorkOrderView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
221
tailbone/templates/workorders/view.mako
Normal file
221
tailbone/templates/workorders/view.mako
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/master/view.mako" />
|
||||||
|
|
||||||
|
## TODO: what was this about?
|
||||||
|
<%def name="content_title()">
|
||||||
|
## ${instance_title}
|
||||||
|
#${instance.id} for ${instance.customer} (${enum.WORKORDER_STATUS[instance.status_code]})
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="object_helpers()">
|
||||||
|
% if instance.status_code not in (enum.WORKORDER_STATUS_DELIVERED, enum.WORKORDER_STATUS_CANCELED):
|
||||||
|
${self.render_workflow_helper()}
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="render_workflow_helper()">
|
||||||
|
<nav class="panel">
|
||||||
|
<p class="panel-heading">Workflow</p>
|
||||||
|
|
||||||
|
% if instance.status_code == enum.WORKORDER_STATUS_SUBMITTED:
|
||||||
|
<div class="panel-block">
|
||||||
|
<div class="buttons">
|
||||||
|
${h.form(url('{}.receive'.format(route_prefix), uuid=instance.uuid), ref='receiveForm')}
|
||||||
|
${h.csrf_token(request)}
|
||||||
|
<b-button type="is-primary"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="fas fa-arrow-right"
|
||||||
|
@click="receive()"
|
||||||
|
:disabled="receiveButtonDisabled">
|
||||||
|
{{ receiveButtonText }}
|
||||||
|
</b-button>
|
||||||
|
${h.end_form()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
|
||||||
|
% if instance.status_code == enum.WORKORDER_STATUS_RECEIVED:
|
||||||
|
<div class="panel-block">
|
||||||
|
<div class="buttons">
|
||||||
|
${h.form(url('{}.await_estimate'.format(route_prefix), uuid=instance.uuid), ref='awaitEstimateForm')}
|
||||||
|
${h.csrf_token(request)}
|
||||||
|
<b-button type="is-primary"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="fas fa-arrow-right"
|
||||||
|
@click="awaitEstimate()"
|
||||||
|
:disabled="awaitEstimateButtonDisabled">
|
||||||
|
{{ awaitEstimateButtonText }}
|
||||||
|
</b-button>
|
||||||
|
${h.end_form()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
|
||||||
|
% if instance.status_code in (enum.WORKORDER_STATUS_RECEIVED, enum.WORKORDER_STATUS_PENDING_ESTIMATE):
|
||||||
|
<div class="panel-block">
|
||||||
|
<div class="buttons">
|
||||||
|
${h.form(url('{}.await_parts'.format(route_prefix), uuid=instance.uuid), ref='awaitPartsForm')}
|
||||||
|
${h.csrf_token(request)}
|
||||||
|
<b-button type="is-primary"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="fas fa-arrow-right"
|
||||||
|
@click="awaitParts()"
|
||||||
|
:disabled="awaitPartsButtonDisabled">
|
||||||
|
{{ awaitPartsButtonText }}
|
||||||
|
</b-button>
|
||||||
|
${h.end_form()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
|
||||||
|
% if instance.status_code in (enum.WORKORDER_STATUS_RECEIVED, enum.WORKORDER_STATUS_PENDING_ESTIMATE, enum.WORKORDER_STATUS_WAITING_FOR_PARTS):
|
||||||
|
<div class="panel-block">
|
||||||
|
<div class="buttons">
|
||||||
|
${h.form(url('{}.work_on_it'.format(route_prefix), uuid=instance.uuid), ref='workOnItForm')}
|
||||||
|
${h.csrf_token(request)}
|
||||||
|
<b-button type="is-primary"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="fas fa-arrow-right"
|
||||||
|
@click="workOnIt()"
|
||||||
|
:disabled="workOnItButtonDisabled">
|
||||||
|
{{ workOnItButtonText }}
|
||||||
|
</b-button>
|
||||||
|
${h.end_form()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
|
||||||
|
% if instance.status_code == enum.WORKORDER_STATUS_WORKING_ON_IT:
|
||||||
|
<div class="panel-block">
|
||||||
|
<div class="buttons">
|
||||||
|
${h.form(url('{}.release'.format(route_prefix), uuid=instance.uuid), ref='releaseForm')}
|
||||||
|
${h.csrf_token(request)}
|
||||||
|
<b-button type="is-primary"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="fas fa-arrow-right"
|
||||||
|
@click="release()"
|
||||||
|
:disabled="releaseButtonDisabled">
|
||||||
|
{{ releaseButtonText }}
|
||||||
|
</b-button>
|
||||||
|
${h.end_form()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
|
||||||
|
% if instance.status_code == enum.WORKORDER_STATUS_RELEASED:
|
||||||
|
<div class="panel-block">
|
||||||
|
<div class="buttons">
|
||||||
|
${h.form(url('{}.deliver'.format(route_prefix), uuid=instance.uuid), ref='deliverForm')}
|
||||||
|
${h.csrf_token(request)}
|
||||||
|
<b-button type="is-primary"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="fas fa-arrow-right"
|
||||||
|
@click="deliver()"
|
||||||
|
:disabled="deliverButtonDisabled">
|
||||||
|
{{ deliverButtonText }}
|
||||||
|
</b-button>
|
||||||
|
${h.end_form()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
|
||||||
|
% if instance.status_code not in (enum.WORKORDER_STATUS_DELIVERED, enum.WORKORDER_STATUS_CANCELED):
|
||||||
|
<div class="panel-block">
|
||||||
|
<p class="is-italic has-text-centered"
|
||||||
|
style="width: 100%;">
|
||||||
|
OR
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="panel-block">
|
||||||
|
<div class="buttons">
|
||||||
|
${h.form(url('{}.cancel'.format(route_prefix), uuid=instance.uuid), ref='cancelForm')}
|
||||||
|
${h.csrf_token(request)}
|
||||||
|
<b-button type="is-warning"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="fas fa-ban"
|
||||||
|
@click="confirmCancel()"
|
||||||
|
:disabled="cancelButtonDisabled">
|
||||||
|
{{ cancelButtonText }}
|
||||||
|
</b-button>
|
||||||
|
${h.end_form()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="modify_this_page_vars()">
|
||||||
|
${parent.modify_this_page_vars()}
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
ThisPageData.receiveButtonDisabled = false
|
||||||
|
ThisPageData.receiveButtonText = "I've received the order from customer"
|
||||||
|
|
||||||
|
ThisPageData.awaitEstimateButtonDisabled = false
|
||||||
|
ThisPageData.awaitEstimateButtonText = "I'm waiting for estimate confirmation"
|
||||||
|
|
||||||
|
ThisPageData.awaitPartsButtonDisabled = false
|
||||||
|
ThisPageData.awaitPartsButtonText = "I'm waiting for parts"
|
||||||
|
|
||||||
|
ThisPageData.workOnItButtonDisabled = false
|
||||||
|
ThisPageData.workOnItButtonText = "I'm working on it"
|
||||||
|
|
||||||
|
ThisPageData.releaseButtonDisabled = false
|
||||||
|
ThisPageData.releaseButtonText = "I've sent this back to customer"
|
||||||
|
|
||||||
|
ThisPageData.deliverButtonDisabled = false
|
||||||
|
ThisPageData.deliverButtonText = "Customer has the completed order!"
|
||||||
|
|
||||||
|
ThisPageData.cancelButtonDisabled = false
|
||||||
|
ThisPageData.cancelButtonText = "Cancel this Work Order"
|
||||||
|
|
||||||
|
ThisPage.methods.receive = function() {
|
||||||
|
this.receiveButtonDisabled = true
|
||||||
|
this.receiveButtonText = "Working, please wait..."
|
||||||
|
this.$refs.receiveForm.submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
ThisPage.methods.awaitEstimate = function() {
|
||||||
|
this.awaitEstimateButtonDisabled = true
|
||||||
|
this.awaitEstimateButtonText = "Working, please wait..."
|
||||||
|
this.$refs.awaitEstimateForm.submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
ThisPage.methods.awaitParts = function() {
|
||||||
|
this.awaitPartsButtonDisabled = true
|
||||||
|
this.awaitPartsButtonText = "Working, please wait..."
|
||||||
|
this.$refs.awaitPartsForm.submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
ThisPage.methods.workOnIt = function() {
|
||||||
|
this.workOnItButtonDisabled = true
|
||||||
|
this.workOnItButtonText = "Working, please wait..."
|
||||||
|
this.$refs.workOnItForm.submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
ThisPage.methods.release = function() {
|
||||||
|
this.releaseButtonDisabled = true
|
||||||
|
this.releaseButtonText = "Working, please wait..."
|
||||||
|
this.$refs.releaseForm.submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
ThisPage.methods.deliver = function() {
|
||||||
|
this.deliverButtonDisabled = true
|
||||||
|
this.deliverButtonText = "Working, please wait..."
|
||||||
|
this.$refs.deliverForm.submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
ThisPage.methods.confirmCancel = function() {
|
||||||
|
if (confirm("Are you sure you wish to cancel this Work Order?")) {
|
||||||
|
this.cancelButtonDisabled = true
|
||||||
|
this.cancelButtonText = "Working, please wait..."
|
||||||
|
this.$refs.cancelForm.submit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
|
${parent.body()}
|
419
tailbone/views/workorders.py
Normal file
419
tailbone/views/workorders.py
Normal file
|
@ -0,0 +1,419 @@
|
||||||
|
# -*- 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Work Order Views
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from rattail.db.model import WorkOrder, WorkOrderEvent
|
||||||
|
|
||||||
|
from webhelpers2.html import HTML
|
||||||
|
|
||||||
|
from tailbone import forms, grids
|
||||||
|
from tailbone.views import MasterView
|
||||||
|
|
||||||
|
|
||||||
|
class WorkOrderView(MasterView):
|
||||||
|
"""
|
||||||
|
Master view for work orders
|
||||||
|
"""
|
||||||
|
model_class = WorkOrder
|
||||||
|
route_prefix = 'workorders'
|
||||||
|
url_prefix = '/workorders'
|
||||||
|
bulk_deletable = True
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
'id': "ID",
|
||||||
|
'status_code': "Status",
|
||||||
|
}
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'id',
|
||||||
|
'customer',
|
||||||
|
'date_received',
|
||||||
|
'date_released',
|
||||||
|
'status_code',
|
||||||
|
]
|
||||||
|
|
||||||
|
form_fields = [
|
||||||
|
'id',
|
||||||
|
'customer',
|
||||||
|
'notes',
|
||||||
|
'date_submitted',
|
||||||
|
'date_received',
|
||||||
|
'date_released',
|
||||||
|
'date_delivered',
|
||||||
|
'status_code',
|
||||||
|
]
|
||||||
|
|
||||||
|
has_rows = True
|
||||||
|
model_row_class = WorkOrderEvent
|
||||||
|
rows_viewable = False
|
||||||
|
|
||||||
|
row_labels = {
|
||||||
|
'type_code': "Event Type",
|
||||||
|
}
|
||||||
|
|
||||||
|
row_grid_columns = [
|
||||||
|
'type_code',
|
||||||
|
'occurred',
|
||||||
|
'user',
|
||||||
|
'note',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, request):
|
||||||
|
super(WorkOrderView, self).__init__(request)
|
||||||
|
app = self.get_rattail_app()
|
||||||
|
self.workorder_handler = app.get_workorder_handler()
|
||||||
|
|
||||||
|
def configure_grid(self, g):
|
||||||
|
super(WorkOrderView, self).configure_grid(g)
|
||||||
|
model = self.model
|
||||||
|
|
||||||
|
# customer
|
||||||
|
g.set_joiner('customer', lambda q: q.join(model.Customer))
|
||||||
|
g.set_sorter('customer', model.Customer.name)
|
||||||
|
g.set_filter('customer', model.Customer.name)
|
||||||
|
|
||||||
|
# status
|
||||||
|
g.set_filter('status_code', model.WorkOrder.status_code,
|
||||||
|
factory=StatusFilter,
|
||||||
|
default_active=True,
|
||||||
|
default_verb='is_active')
|
||||||
|
g.set_enum('status_code', self.enum.WORKORDER_STATUS)
|
||||||
|
|
||||||
|
g.set_sort_defaults('id', 'desc')
|
||||||
|
|
||||||
|
g.set_link('id')
|
||||||
|
g.set_link('customer')
|
||||||
|
|
||||||
|
def grid_extra_class(self, workorder, i):
|
||||||
|
if workorder.status_code == self.enum.WORKORDER_STATUS_CANCELED:
|
||||||
|
return 'warning'
|
||||||
|
|
||||||
|
def configure_form(self, f):
|
||||||
|
super(WorkOrderView, self).configure_form(f)
|
||||||
|
model = self.model
|
||||||
|
use_buefy = self.get_use_buefy()
|
||||||
|
SelectWidget = forms.widgets.JQuerySelectWidget
|
||||||
|
|
||||||
|
# id
|
||||||
|
if self.creating:
|
||||||
|
f.remove_field('id')
|
||||||
|
else:
|
||||||
|
f.set_readonly('id')
|
||||||
|
|
||||||
|
# customer
|
||||||
|
if self.creating:
|
||||||
|
f.replace('customer', 'customer_uuid')
|
||||||
|
f.set_label('customer_uuid', "Customer")
|
||||||
|
f.set_widget('customer_uuid',
|
||||||
|
forms.widgets.make_customer_widget(self.request))
|
||||||
|
f.set_input_handler('customer_uuid', 'customerChanged')
|
||||||
|
else:
|
||||||
|
f.set_readonly('customer')
|
||||||
|
f.set_renderer('customer', self.render_customer)
|
||||||
|
|
||||||
|
# notes
|
||||||
|
f.set_type('notes', 'text')
|
||||||
|
|
||||||
|
# status_code
|
||||||
|
if self.creating:
|
||||||
|
f.remove('status_code')
|
||||||
|
else:
|
||||||
|
f.set_enum('status_code', self.enum.WORKORDER_STATUS)
|
||||||
|
f.set_renderer('status_code', self.render_status_code)
|
||||||
|
if not self.has_perm('edit_status'):
|
||||||
|
f.set_readonly('status_code')
|
||||||
|
|
||||||
|
# date fields
|
||||||
|
f.set_type('date_submitted', 'date_jquery')
|
||||||
|
f.set_type('date_received', 'date_jquery')
|
||||||
|
f.set_type('date_released', 'date_jquery')
|
||||||
|
f.set_type('date_delivered', 'date_jquery')
|
||||||
|
if self.creating:
|
||||||
|
f.remove('date_submitted',
|
||||||
|
'date_received',
|
||||||
|
'date_released',
|
||||||
|
'date_delivered')
|
||||||
|
elif not self.has_perm('edit_status'):
|
||||||
|
f.set_readonly('date_submitted')
|
||||||
|
f.set_readonly('date_received')
|
||||||
|
f.set_readonly('date_released')
|
||||||
|
f.set_readonly('date_delivered')
|
||||||
|
|
||||||
|
def objectify(self, form, data=None):
|
||||||
|
"""
|
||||||
|
Supplements the default logic as follows:
|
||||||
|
|
||||||
|
If creating a new Work Order, will automatically set its status to
|
||||||
|
"submitted" and its ``date_submitted`` to the current date.
|
||||||
|
"""
|
||||||
|
if data is None:
|
||||||
|
data = form.validated
|
||||||
|
|
||||||
|
# first let deform do its thing. if editing, this will update
|
||||||
|
# the record like we want. but if creating, this will
|
||||||
|
# populate the initial object *without* adding it to session,
|
||||||
|
# which is also what we want, so that we can "replace" the new
|
||||||
|
# object with one the handler creates, below
|
||||||
|
workorder = form.schema.objectify(data, context=form.model_instance)
|
||||||
|
|
||||||
|
if self.creating:
|
||||||
|
|
||||||
|
# now make the "real" work order
|
||||||
|
data = dict([(key, getattr(workorder, key))
|
||||||
|
for key in data])
|
||||||
|
workorder = self.workorder_handler.make_workorder(self.Session(), **data)
|
||||||
|
|
||||||
|
return workorder
|
||||||
|
|
||||||
|
def render_status_code(self, obj, field):
|
||||||
|
status_code = getattr(obj, field)
|
||||||
|
if status_code is None:
|
||||||
|
return ""
|
||||||
|
if status_code in self.enum.WORKORDER_STATUS:
|
||||||
|
text = self.enum.WORKORDER_STATUS[status_code]
|
||||||
|
if status_code == self.enum.WORKORDER_STATUS_CANCELED:
|
||||||
|
use_buefy = self.get_use_buefy()
|
||||||
|
if use_buefy:
|
||||||
|
return HTML.tag('span', class_='has-text-danger', c=text)
|
||||||
|
else:
|
||||||
|
return HTML.tag('span', style='color: red;', c=text)
|
||||||
|
return text
|
||||||
|
return str(status_code)
|
||||||
|
|
||||||
|
def get_row_data(self, workorder):
|
||||||
|
model = self.model
|
||||||
|
return self.Session.query(model.WorkOrderEvent)\
|
||||||
|
.filter(model.WorkOrderEvent.workorder == workorder)
|
||||||
|
|
||||||
|
def get_parent(self, event):
|
||||||
|
return event.workorder
|
||||||
|
|
||||||
|
def configure_row_grid(self, g):
|
||||||
|
super(WorkOrderView, self).configure_row_grid(g)
|
||||||
|
g.set_enum('type_code', self.enum.WORKORDER_EVENT)
|
||||||
|
g.set_sort_defaults('occurred')
|
||||||
|
|
||||||
|
def receive(self):
|
||||||
|
"""
|
||||||
|
Sets work order status to "received".
|
||||||
|
"""
|
||||||
|
workorder = self.get_instance()
|
||||||
|
self.workorder_handler.receive(workorder)
|
||||||
|
self.Session.flush()
|
||||||
|
return self.redirect(self.get_action_url('view', workorder))
|
||||||
|
|
||||||
|
def await_estimate(self):
|
||||||
|
"""
|
||||||
|
Sets work order status to "awaiting estimate confirmation".
|
||||||
|
"""
|
||||||
|
workorder = self.get_instance()
|
||||||
|
self.workorder_handler.await_estimate(workorder)
|
||||||
|
self.Session.flush()
|
||||||
|
return self.redirect(self.get_action_url('view', workorder))
|
||||||
|
|
||||||
|
def await_parts(self):
|
||||||
|
"""
|
||||||
|
Sets work order status to "awaiting parts".
|
||||||
|
"""
|
||||||
|
workorder = self.get_instance()
|
||||||
|
self.workorder_handler.await_parts(workorder)
|
||||||
|
self.Session.flush()
|
||||||
|
return self.redirect(self.get_action_url('view', workorder))
|
||||||
|
|
||||||
|
def work_on_it(self):
|
||||||
|
"""
|
||||||
|
Sets work order status to "working on it".
|
||||||
|
"""
|
||||||
|
workorder = self.get_instance()
|
||||||
|
self.workorder_handler.work_on_it(workorder)
|
||||||
|
self.Session.flush()
|
||||||
|
return self.redirect(self.get_action_url('view', workorder))
|
||||||
|
|
||||||
|
def release(self):
|
||||||
|
"""
|
||||||
|
Sets work order status to "released".
|
||||||
|
"""
|
||||||
|
workorder = self.get_instance()
|
||||||
|
self.workorder_handler.release(workorder)
|
||||||
|
self.Session.flush()
|
||||||
|
return self.redirect(self.get_action_url('view', workorder))
|
||||||
|
|
||||||
|
def deliver(self):
|
||||||
|
"""
|
||||||
|
Sets work order status to "delivered".
|
||||||
|
"""
|
||||||
|
workorder = self.get_instance()
|
||||||
|
self.workorder_handler.deliver(workorder)
|
||||||
|
self.Session.flush()
|
||||||
|
return self.redirect(self.get_action_url('view', workorder))
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
"""
|
||||||
|
Sets work order status to "canceled".
|
||||||
|
"""
|
||||||
|
workorder = self.get_instance()
|
||||||
|
self.workorder_handler.cancel(workorder)
|
||||||
|
self.Session.flush()
|
||||||
|
return self.redirect(self.get_action_url('view', workorder))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def defaults(cls, config):
|
||||||
|
cls._defaults(config)
|
||||||
|
cls._workorder_defaults(config)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _workorder_defaults(cls, config):
|
||||||
|
route_prefix = cls.get_route_prefix()
|
||||||
|
permission_prefix = cls.get_permission_prefix()
|
||||||
|
instance_url_prefix = cls.get_instance_url_prefix()
|
||||||
|
model_title = cls.get_model_title()
|
||||||
|
|
||||||
|
# perm for editing status
|
||||||
|
config.add_tailbone_permission(
|
||||||
|
permission_prefix,
|
||||||
|
'{}.edit_status'.format(permission_prefix),
|
||||||
|
"Directly edit status and related fields for {}".format(model_title))
|
||||||
|
|
||||||
|
# receive
|
||||||
|
config.add_route('{}.receive'.format(route_prefix),
|
||||||
|
'{}/receive'.format(instance_url_prefix),
|
||||||
|
request_method='POST')
|
||||||
|
config.add_view(cls, attr='receive',
|
||||||
|
route_name='{}.receive'.format(route_prefix),
|
||||||
|
permission='{}.edit'.format(permission_prefix))
|
||||||
|
|
||||||
|
# await_estimate
|
||||||
|
config.add_route('{}.await_estimate'.format(route_prefix),
|
||||||
|
'{}/await-estimate'.format(instance_url_prefix),
|
||||||
|
request_method='POST')
|
||||||
|
config.add_view(cls, attr='await_estimate',
|
||||||
|
route_name='{}.await_estimate'.format(route_prefix),
|
||||||
|
permission='{}.edit'.format(permission_prefix))
|
||||||
|
|
||||||
|
# await_parts
|
||||||
|
config.add_route('{}.await_parts'.format(route_prefix),
|
||||||
|
'{}/await-parts'.format(instance_url_prefix),
|
||||||
|
request_method='POST')
|
||||||
|
config.add_view(cls, attr='await_parts',
|
||||||
|
route_name='{}.await_parts'.format(route_prefix),
|
||||||
|
permission='{}.edit'.format(permission_prefix))
|
||||||
|
|
||||||
|
# work_on_it
|
||||||
|
config.add_route('{}.work_on_it'.format(route_prefix),
|
||||||
|
'{}/work-on-it'.format(instance_url_prefix),
|
||||||
|
request_method='POST')
|
||||||
|
config.add_view(cls, attr='work_on_it',
|
||||||
|
route_name='{}.work_on_it'.format(route_prefix),
|
||||||
|
permission='{}.edit'.format(permission_prefix))
|
||||||
|
|
||||||
|
# release
|
||||||
|
config.add_route('{}.release'.format(route_prefix),
|
||||||
|
'{}/release'.format(instance_url_prefix),
|
||||||
|
request_method='POST')
|
||||||
|
config.add_view(cls, attr='release',
|
||||||
|
route_name='{}.release'.format(route_prefix),
|
||||||
|
permission='{}.edit'.format(permission_prefix))
|
||||||
|
|
||||||
|
# deliver
|
||||||
|
config.add_route('{}.deliver'.format(route_prefix),
|
||||||
|
'{}/deliver'.format(instance_url_prefix),
|
||||||
|
request_method='POST')
|
||||||
|
config.add_view(cls, attr='deliver',
|
||||||
|
route_name='{}.deliver'.format(route_prefix),
|
||||||
|
permission='{}.edit'.format(permission_prefix))
|
||||||
|
|
||||||
|
# cancel
|
||||||
|
config.add_route('{}.cancel'.format(route_prefix),
|
||||||
|
'{}/cancel'.format(instance_url_prefix),
|
||||||
|
request_method='POST')
|
||||||
|
config.add_view(cls, attr='cancel',
|
||||||
|
route_name='{}.cancel'.format(route_prefix),
|
||||||
|
permission='{}.edit'.format(permission_prefix))
|
||||||
|
|
||||||
|
|
||||||
|
class StatusFilter(grids.filters.AlchemyIntegerFilter):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(StatusFilter, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
from drild import enum
|
||||||
|
|
||||||
|
self.active_status_codes = [
|
||||||
|
# enum.WORKORDER_STATUS_CREATED,
|
||||||
|
enum.WORKORDER_STATUS_SUBMITTED,
|
||||||
|
enum.WORKORDER_STATUS_RECEIVED,
|
||||||
|
enum.WORKORDER_STATUS_PENDING_ESTIMATE,
|
||||||
|
enum.WORKORDER_STATUS_WAITING_FOR_PARTS,
|
||||||
|
enum.WORKORDER_STATUS_WORKING_ON_IT,
|
||||||
|
enum.WORKORDER_STATUS_RELEASED,
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verb_labels(self):
|
||||||
|
labels = dict(super(StatusFilter, self).verb_labels)
|
||||||
|
labels['is_active'] = "Is Active"
|
||||||
|
labels['not_active'] = "Is Not Active"
|
||||||
|
return labels
|
||||||
|
|
||||||
|
@property
|
||||||
|
def valueless_verbs(self):
|
||||||
|
verbs = list(super(StatusFilter, self).valueless_verbs)
|
||||||
|
verbs.extend([
|
||||||
|
'is_active',
|
||||||
|
'not_active',
|
||||||
|
])
|
||||||
|
return verbs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_verbs(self):
|
||||||
|
verbs = list(super(StatusFilter, self).default_verbs)
|
||||||
|
verbs.insert(0, 'is_active')
|
||||||
|
verbs.insert(1, 'not_active')
|
||||||
|
return verbs
|
||||||
|
|
||||||
|
def filter_is_active(self, query, value):
|
||||||
|
return query.filter(
|
||||||
|
WorkOrder.status_code.in_(self.active_status_codes))
|
||||||
|
|
||||||
|
def filter_not_active(self, query, value):
|
||||||
|
return query.filter(sa.or_(
|
||||||
|
~WorkOrder.status_code.in_(self.active_status_codes),
|
||||||
|
WorkOrder.status_code == None,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
WorkOrderView = kwargs.get('WorkOrderView', base['WorkOrderView'])
|
||||||
|
WorkOrderView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
Loading…
Reference in a new issue