Compare commits
26 commits
Author | SHA1 | Date | |
---|---|---|---|
e150453801 | |||
e2582ffec5 | |||
a6508154cb | |||
7348eec671 | |||
4221fa50dd | |||
e0ebd43e7a | |||
c7ee9de9eb | |||
950db697a0 | |||
358b3b75a5 | |||
7e559a01b3 | |||
23bdde245a | |||
2c269b640b | |||
![]() |
f1c8ffedda | ||
![]() |
aace6033c5 | ||
![]() |
7171c7fb06 | ||
![]() |
993f066f2c | ||
![]() |
980031f524 | ||
![]() |
bcaf0d08bc | ||
![]() |
ac439c949b | ||
![]() |
20b3f87dbe | ||
![]() |
9e55717041 | ||
![]() |
772b6610cb | ||
![]() |
3f27f626df | ||
![]() |
29743e70b7 | ||
![]() |
54220601ed | ||
![]() |
9a6f8970ae |
16 changed files with 226 additions and 67 deletions
52
CHANGELOG.md
52
CHANGELOG.md
|
@ -5,6 +5,58 @@ All notable changes to Tailbone will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## v0.22.7 (2025-02-19)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- stop using old config for logo image url on login page
|
||||||
|
- fix warning msg for deprecated Grid param
|
||||||
|
|
||||||
|
## v0.22.6 (2025-02-01)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- register vue3 form component for products -> make batch
|
||||||
|
|
||||||
|
## v0.22.5 (2024-12-16)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- whoops this is latest rattail
|
||||||
|
- require newer rattail lib
|
||||||
|
- require newer wuttaweb
|
||||||
|
- let caller request safe HTML literal for rendered grid table
|
||||||
|
|
||||||
|
## v0.22.4 (2024-11-22)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- avoid error in product search for duplicated key
|
||||||
|
- use vmodel for confirm password widget input
|
||||||
|
|
||||||
|
## v0.22.3 (2024-11-19)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- avoid error for trainwreck query when not a customer
|
||||||
|
|
||||||
|
## v0.22.2 (2024-11-18)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- use local/custom enum for continuum operations
|
||||||
|
- add basic master view for Product Costs
|
||||||
|
- show continuum operation type when viewing version history
|
||||||
|
- always define `app` attr for ViewSupplement
|
||||||
|
- avoid deprecated import
|
||||||
|
|
||||||
|
## v0.22.1 (2024-11-02)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- fix submit button for running problem report
|
||||||
|
- avoid deprecated grid method
|
||||||
|
|
||||||
## v0.22.0 (2024-10-22)
|
## v0.22.0 (2024-10-22)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
|
|
|
@ -27,10 +27,10 @@ templates_path = ['_templates']
|
||||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||||
|
|
||||||
intersphinx_mapping = {
|
intersphinx_mapping = {
|
||||||
'rattail': ('https://rattailproject.org/docs/rattail/', None),
|
'rattail': ('https://docs.wuttaproject.org/rattail/', None),
|
||||||
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
|
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
|
||||||
'wuttaweb': ('https://rattailproject.org/docs/wuttaweb/', None),
|
'wuttaweb': ('https://docs.wuttaproject.org/wuttaweb/', None),
|
||||||
'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None),
|
'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None),
|
||||||
}
|
}
|
||||||
|
|
||||||
# allow todo entries to show up
|
# allow todo entries to show up
|
||||||
|
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "Tailbone"
|
name = "Tailbone"
|
||||||
version = "0.22.0"
|
version = "0.22.7"
|
||||||
description = "Backoffice Web Application for Rattail"
|
description = "Backoffice Web Application for Rattail"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
||||||
|
@ -53,13 +53,13 @@ dependencies = [
|
||||||
"pyramid_mako",
|
"pyramid_mako",
|
||||||
"pyramid_retry",
|
"pyramid_retry",
|
||||||
"pyramid_tm",
|
"pyramid_tm",
|
||||||
"rattail[db,bouncer]>=0.18.5",
|
"rattail[db,bouncer]>=0.20.1",
|
||||||
"sa-filters",
|
"sa-filters",
|
||||||
"simplejson",
|
"simplejson",
|
||||||
"transaction",
|
"transaction",
|
||||||
"waitress",
|
"waitress",
|
||||||
"WebHelpers2",
|
"WebHelpers2",
|
||||||
"WuttaWeb>=0.14.0",
|
"WuttaWeb>=0.21.0",
|
||||||
"zope.sqlalchemy>=1.5",
|
"zope.sqlalchemy>=1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,6 @@ Tailbone Web API - Master View
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from rattail.config import parse_bool
|
|
||||||
from rattail.db.util import get_fieldnames
|
from rattail.db.util import get_fieldnames
|
||||||
|
|
||||||
from cornice import resource, Service
|
from cornice import resource, Service
|
||||||
|
@ -185,7 +184,7 @@ class APIMasterView(APIView):
|
||||||
if sortcol:
|
if sortcol:
|
||||||
spec = {
|
spec = {
|
||||||
'field': sortcol.field_name,
|
'field': sortcol.field_name,
|
||||||
'direction': 'asc' if parse_bool(self.request.params['ascending']) else 'desc',
|
'direction': 'asc' if self.config.parse_bool(self.request.params['ascending']) else 'desc',
|
||||||
}
|
}
|
||||||
if sortcol.model_name:
|
if sortcol.model_name:
|
||||||
spec['model'] = sortcol.model_name
|
spec['model'] = sortcol.model_name
|
||||||
|
|
|
@ -62,6 +62,17 @@ def make_rattail_config(settings):
|
||||||
# nb. this is for compaibility with wuttaweb
|
# nb. this is for compaibility with wuttaweb
|
||||||
settings['wutta_config'] = rattail_config
|
settings['wutta_config'] = rattail_config
|
||||||
|
|
||||||
|
# must import all sqlalchemy models before things get rolling,
|
||||||
|
# otherwise can have errors about continuum TransactionMeta class
|
||||||
|
# not yet mapped, when relevant pages are first requested...
|
||||||
|
# cf. https://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/database/sqlalchemy.html#importing-all-sqlalchemy-models
|
||||||
|
# hat tip to https://stackoverflow.com/a/59241485
|
||||||
|
if getattr(rattail_config, 'tempmon_engine', None):
|
||||||
|
from rattail_tempmon.db import model as tempmon_model, Session as TempmonSession
|
||||||
|
tempmon_session = TempmonSession()
|
||||||
|
tempmon_session.query(tempmon_model.Appliance).first()
|
||||||
|
tempmon_session.close()
|
||||||
|
|
||||||
# configure database sessions
|
# configure database sessions
|
||||||
if hasattr(rattail_config, 'appdb_engine'):
|
if hasattr(rattail_config, 'appdb_engine'):
|
||||||
tailbone.db.Session.configure(bind=rattail_config.appdb_engine)
|
tailbone.db.Session.configure(bind=rattail_config.appdb_engine)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -270,9 +270,21 @@ class VersionDiff(Diff):
|
||||||
for field in self.fields:
|
for field in self.fields:
|
||||||
values[field] = {'before': self.render_old_value(field),
|
values[field] = {'before': self.render_old_value(field),
|
||||||
'after': self.render_new_value(field)}
|
'after': self.render_new_value(field)}
|
||||||
|
|
||||||
|
operation = None
|
||||||
|
if self.version.operation_type == continuum.Operation.INSERT:
|
||||||
|
operation = 'INSERT'
|
||||||
|
elif self.version.operation_type == continuum.Operation.UPDATE:
|
||||||
|
operation = 'UPDATE'
|
||||||
|
elif self.version.operation_type == continuum.Operation.DELETE:
|
||||||
|
operation = 'DELETE'
|
||||||
|
else:
|
||||||
|
operation = self.version.operation_type
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'key': id(self.version),
|
'key': id(self.version),
|
||||||
'model_title': self.title,
|
'model_title': self.title,
|
||||||
|
'operation': operation,
|
||||||
'diff_class': self.nature,
|
'diff_class': self.nature,
|
||||||
'fields': self.fields,
|
'fields': self.fields,
|
||||||
'values': values,
|
'values': values,
|
||||||
|
|
|
@ -235,7 +235,7 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
if 'pageable' in kwargs:
|
if 'pageable' in kwargs:
|
||||||
warnings.warn("pageable param is deprecated for Grid(); "
|
warnings.warn("pageable param is deprecated for Grid(); "
|
||||||
"please use vue_tagname param instead",
|
"please use paginated param instead",
|
||||||
DeprecationWarning, stacklevel=2)
|
DeprecationWarning, stacklevel=2)
|
||||||
kwargs.setdefault('paginated', kwargs.pop('pageable'))
|
kwargs.setdefault('paginated', kwargs.pop('pageable'))
|
||||||
|
|
||||||
|
@ -1223,6 +1223,7 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
def render_table_element(self, template='/grids/b-table.mako',
|
def render_table_element(self, template='/grids/b-table.mako',
|
||||||
data_prop='gridData', empty_labels=False,
|
data_prop='gridData', empty_labels=False,
|
||||||
|
literal=False,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
"""
|
"""
|
||||||
This is intended for ad-hoc "small" grids with static data. Renders
|
This is intended for ad-hoc "small" grids with static data. Renders
|
||||||
|
@ -1239,7 +1240,10 @@ class Grid(WuttaGrid):
|
||||||
if context['paginated']:
|
if context['paginated']:
|
||||||
context.setdefault('per_page', 20)
|
context.setdefault('per_page', 20)
|
||||||
context['view_click_handler'] = self.get_view_click_handler()
|
context['view_click_handler'] = self.get_view_click_handler()
|
||||||
return render(template, context)
|
result = render(template, context)
|
||||||
|
if literal:
|
||||||
|
result = HTML.literal(result)
|
||||||
|
return result
|
||||||
|
|
||||||
def get_view_click_handler(self):
|
def get_view_click_handler(self):
|
||||||
""" """
|
""" """
|
||||||
|
|
|
@ -394,6 +394,11 @@ class TailboneMenuHandler(WuttaMenuHandler):
|
||||||
'route': 'products',
|
'route': 'products',
|
||||||
'perm': 'products.list',
|
'perm': 'products.list',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'title': "Product Costs",
|
||||||
|
'route': 'product_costs',
|
||||||
|
'perm': 'product_costs.list',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'title': "Departments",
|
'title': "Departments",
|
||||||
'route': 'departments',
|
'route': 'departments',
|
||||||
|
@ -451,6 +456,11 @@ class TailboneMenuHandler(WuttaMenuHandler):
|
||||||
'route': 'vendors',
|
'route': 'vendors',
|
||||||
'perm': 'vendors.list',
|
'perm': 'vendors.list',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'title': "Product Costs",
|
||||||
|
'route': 'product_costs',
|
||||||
|
'perm': 'product_costs.list',
|
||||||
|
},
|
||||||
{'type': 'sep'},
|
{'type': 'sep'},
|
||||||
{
|
{
|
||||||
'title': "Ordering",
|
'title': "Ordering",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<div i18n:domain="deform" tal:omit-tag=""
|
<div i18n:domain="deform" tal:omit-tag=""
|
||||||
tal:define="oid oid|field.oid;
|
tal:define="oid oid|field.oid;
|
||||||
name name|field.name;
|
name name|field.name;
|
||||||
|
vmodel vmodel|'field_model_' + name;
|
||||||
css_class css_class|field.widget.css_class;
|
css_class css_class|field.widget.css_class;
|
||||||
style style|field.widget.style;">
|
style style|field.widget.style;">
|
||||||
|
|
||||||
|
@ -8,7 +9,7 @@
|
||||||
${field.start_mapping()}
|
${field.start_mapping()}
|
||||||
<b-input type="password"
|
<b-input type="password"
|
||||||
name="${name}"
|
name="${name}"
|
||||||
value="${field.widget.redisplay and cstruct or ''}"
|
v-model="${vmodel}"
|
||||||
tal:attributes="class string: form-control ${css_class or ''};
|
tal:attributes="class string: form-control ${css_class or ''};
|
||||||
style style;
|
style style;
|
||||||
attributes|field.widget.attributes|{};"
|
attributes|field.widget.attributes|{};"
|
||||||
|
@ -18,7 +19,6 @@
|
||||||
</b-input>
|
</b-input>
|
||||||
<b-input type="password"
|
<b-input type="password"
|
||||||
name="${name}-confirm"
|
name="${name}-confirm"
|
||||||
value="${field.widget.redisplay and confirm or ''}"
|
|
||||||
tal:attributes="class string: form-control ${css_class or ''};
|
tal:attributes="class string: form-control ${css_class or ''};
|
||||||
style style;
|
style style;
|
||||||
confirm_attributes|field.widget.confirm_attributes|{};"
|
confirm_attributes|field.widget.confirm_attributes|{};"
|
||||||
|
|
|
@ -196,6 +196,7 @@
|
||||||
|
|
||||||
<p class="block has-text-weight-bold">
|
<p class="block has-text-weight-bold">
|
||||||
{{ version.model_title }}
|
{{ version.model_title }}
|
||||||
|
({{ version.operation }})
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table class="diff monospace is-size-7"
|
<table class="diff monospace is-size-7"
|
||||||
|
|
|
@ -55,19 +55,20 @@
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="render_form_template()">
|
<%def name="render_form_template()">
|
||||||
<script type="text/x-template" id="${form.component}-template">
|
<script type="text/x-template" id="${form.vue_tagname}-template">
|
||||||
${self.render_form_innards()}
|
${self.render_form_innards()}
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_vue_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_vue_vars()}
|
${parent.modify_vue_vars()}
|
||||||
|
<% request.register_component(form.vue_tagname, form.vue_component) %>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
## TODO: ugh, an awful lot of duplicated code here (from /forms/deform.mako)
|
## TODO: ugh, an awful lot of duplicated code here (from /forms/deform.mako)
|
||||||
|
|
||||||
let ${form.vue_component} = {
|
let ${form.vue_component} = {
|
||||||
template: '#${form.component}-template',
|
template: '#${form.vue_tagname}-template',
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
## TODO: deprecate / remove the latter option here
|
## TODO: deprecate / remove the latter option here
|
||||||
|
|
|
@ -45,11 +45,10 @@
|
||||||
<b-button @click="runReportShowDialog = false">
|
<b-button @click="runReportShowDialog = false">
|
||||||
Cancel
|
Cancel
|
||||||
</b-button>
|
</b-button>
|
||||||
${h.form(master.get_action_url('execute', instance))}
|
${h.form(master.get_action_url('execute', instance), **{'@submit': 'runReportSubmitting = true'})}
|
||||||
${h.csrf_token(request)}
|
${h.csrf_token(request)}
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
@click="runReportSubmitting = true"
|
|
||||||
:disabled="runReportSubmitting"
|
:disabled="runReportSubmitting"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="arrow-circle-right">
|
icon-left="arrow-circle-right">
|
||||||
|
|
|
@ -44,28 +44,6 @@ class UserLogin(colander.MappingSchema):
|
||||||
widget=dfwidget.PasswordWidget())
|
widget=dfwidget.PasswordWidget())
|
||||||
|
|
||||||
|
|
||||||
@colander.deferred
|
|
||||||
def current_password_correct(node, kw):
|
|
||||||
request = kw['request']
|
|
||||||
app = request.rattail_config.get_app()
|
|
||||||
auth = app.get_auth_handler()
|
|
||||||
user = kw['user']
|
|
||||||
def validate(node, value):
|
|
||||||
if not auth.authenticate_user(Session(), user.username, value):
|
|
||||||
raise colander.Invalid(node, "The password is incorrect")
|
|
||||||
return validate
|
|
||||||
|
|
||||||
|
|
||||||
class ChangePassword(colander.MappingSchema):
|
|
||||||
|
|
||||||
current_password = colander.SchemaNode(colander.String(),
|
|
||||||
widget=dfwidget.PasswordWidget(),
|
|
||||||
validator=current_password_correct)
|
|
||||||
|
|
||||||
new_password = colander.SchemaNode(colander.String(),
|
|
||||||
widget=dfwidget.CheckedPasswordWidget())
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationView(View):
|
class AuthenticationView(View):
|
||||||
|
|
||||||
def forbidden(self):
|
def forbidden(self):
|
||||||
|
@ -116,10 +94,6 @@ class AuthenticationView(View):
|
||||||
else:
|
else:
|
||||||
self.request.session.flash("Invalid username or password", 'error')
|
self.request.session.flash("Invalid username or password", 'error')
|
||||||
|
|
||||||
image_url = self.rattail_config.get(
|
|
||||||
'tailbone', 'main_image_url',
|
|
||||||
default=self.request.static_url('tailbone:static/img/home_logo.png'))
|
|
||||||
|
|
||||||
# nb. hacky..but necessary, to add the refs, for autofocus
|
# nb. hacky..but necessary, to add the refs, for autofocus
|
||||||
# (also add key handler, so ENTER acts like TAB)
|
# (also add key handler, so ENTER acts like TAB)
|
||||||
dform = form.make_deform_form()
|
dform = form.make_deform_form()
|
||||||
|
@ -132,7 +106,6 @@ class AuthenticationView(View):
|
||||||
return {
|
return {
|
||||||
'form': form,
|
'form': form,
|
||||||
'referrer': referrer,
|
'referrer': referrer,
|
||||||
'image_url': image_url,
|
|
||||||
'index_title': app.get_node_title(),
|
'index_title': app.get_node_title(),
|
||||||
'help_url': global_help_url(self.rattail_config),
|
'help_url': global_help_url(self.rattail_config),
|
||||||
}
|
}
|
||||||
|
@ -181,7 +154,23 @@ class AuthenticationView(View):
|
||||||
self.request.user))
|
self.request.user))
|
||||||
return self.redirect(self.request.get_referrer())
|
return self.redirect(self.request.get_referrer())
|
||||||
|
|
||||||
schema = ChangePassword().bind(user=self.request.user, request=self.request)
|
def check_user_password(node, value):
|
||||||
|
auth = self.app.get_auth_handler()
|
||||||
|
user = self.request.user
|
||||||
|
if not auth.check_user_password(user, value):
|
||||||
|
node.raise_invalid("The password is incorrect")
|
||||||
|
|
||||||
|
schema = colander.Schema()
|
||||||
|
|
||||||
|
schema.add(colander.SchemaNode(colander.String(),
|
||||||
|
name='current_password',
|
||||||
|
widget=dfwidget.PasswordWidget(),
|
||||||
|
validator=check_user_password))
|
||||||
|
|
||||||
|
schema.add(colander.SchemaNode(colander.String(),
|
||||||
|
name='new_password',
|
||||||
|
widget=dfwidget.CheckedPasswordWidget()))
|
||||||
|
|
||||||
form = forms.Form(schema=schema, request=self.request)
|
form = forms.Form(schema=schema, request=self.request)
|
||||||
if form.validate():
|
if form.validate():
|
||||||
auth = self.app.get_auth_handler()
|
auth = self.app.get_auth_handler()
|
||||||
|
|
|
@ -412,7 +412,7 @@ class MasterView(View):
|
||||||
session = self.Session()
|
session = self.Session()
|
||||||
kwargs.setdefault('paginated', False)
|
kwargs.setdefault('paginated', False)
|
||||||
grid = self.make_grid(session=session, **kwargs)
|
grid = self.make_grid(session=session, **kwargs)
|
||||||
return grid.make_visible_data()
|
return grid.get_visible_data()
|
||||||
|
|
||||||
def get_grid_columns(self):
|
def get_grid_columns(self):
|
||||||
"""
|
"""
|
||||||
|
@ -903,7 +903,7 @@ class MasterView(View):
|
||||||
|
|
||||||
def valid_employee_uuid(self, node, value):
|
def valid_employee_uuid(self, node, value):
|
||||||
if value:
|
if value:
|
||||||
model = self.model
|
model = self.app.model
|
||||||
employee = self.Session.get(model.Employee, value)
|
employee = self.Session.get(model.Employee, value)
|
||||||
if not employee:
|
if not employee:
|
||||||
node.raise_invalid("Employee not found")
|
node.raise_invalid("Employee not found")
|
||||||
|
@ -939,7 +939,7 @@ class MasterView(View):
|
||||||
|
|
||||||
def valid_vendor_uuid(self, node, value):
|
def valid_vendor_uuid(self, node, value):
|
||||||
if value:
|
if value:
|
||||||
model = self.model
|
model = self.app.model
|
||||||
vendor = self.Session.get(model.Vendor, value)
|
vendor = self.Session.get(model.Vendor, value)
|
||||||
if not vendor:
|
if not vendor:
|
||||||
node.raise_invalid("Vendor not found")
|
node.raise_invalid("Vendor not found")
|
||||||
|
@ -1382,7 +1382,7 @@ class MasterView(View):
|
||||||
return classes
|
return classes
|
||||||
|
|
||||||
def make_revisions_grid(self, obj, empty_data=False):
|
def make_revisions_grid(self, obj, empty_data=False):
|
||||||
model = self.model
|
model = self.app.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
row_url = lambda txn, i: self.request.route_url(f'{route_prefix}.version',
|
row_url = lambda txn, i: self.request.route_url(f'{route_prefix}.version',
|
||||||
uuid=obj.uuid,
|
uuid=obj.uuid,
|
||||||
|
@ -1710,7 +1710,7 @@ class MasterView(View):
|
||||||
kwargs.setdefault('paginated', False)
|
kwargs.setdefault('paginated', False)
|
||||||
kwargs.setdefault('sortable', sort)
|
kwargs.setdefault('sortable', sort)
|
||||||
grid = self.make_row_grid(session=session, **kwargs)
|
grid = self.make_row_grid(session=session, **kwargs)
|
||||||
return grid.make_visible_data()
|
return grid.get_visible_data()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_row_url_prefix(cls):
|
def get_row_url_prefix(cls):
|
||||||
|
@ -2153,7 +2153,7 @@ class MasterView(View):
|
||||||
Thread target for executing an object.
|
Thread target for executing an object.
|
||||||
"""
|
"""
|
||||||
app = self.get_rattail_app()
|
app = self.get_rattail_app()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
session = app.make_session()
|
session = app.make_session()
|
||||||
obj = self.get_instance_for_key(key, session)
|
obj = self.get_instance_for_key(key, session)
|
||||||
user = session.get(model.User, user_uuid)
|
user = session.get(model.User, user_uuid)
|
||||||
|
@ -2594,7 +2594,7 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
# nb. self.Session may differ, so use tailbone.db.Session
|
# nb. self.Session may differ, so use tailbone.db.Session
|
||||||
session = Session()
|
session = Session()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
|
|
||||||
info = session.query(model.TailbonePageHelp)\
|
info = session.query(model.TailbonePageHelp)\
|
||||||
|
@ -2617,7 +2617,7 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
# nb. self.Session may differ, so use tailbone.db.Session
|
# nb. self.Session may differ, so use tailbone.db.Session
|
||||||
session = Session()
|
session = Session()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
|
|
||||||
info = session.query(model.TailbonePageHelp)\
|
info = session.query(model.TailbonePageHelp)\
|
||||||
|
@ -2639,7 +2639,7 @@ class MasterView(View):
|
||||||
|
|
||||||
# nb. self.Session may differ, so use tailbone.db.Session
|
# nb. self.Session may differ, so use tailbone.db.Session
|
||||||
session = Session()
|
session = Session()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
schema = colander.Schema()
|
schema = colander.Schema()
|
||||||
|
|
||||||
|
@ -2673,7 +2673,7 @@ class MasterView(View):
|
||||||
|
|
||||||
# nb. self.Session may differ, so use tailbone.db.Session
|
# nb. self.Session may differ, so use tailbone.db.Session
|
||||||
session = Session()
|
session = Session()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
schema = colander.Schema()
|
schema = colander.Schema()
|
||||||
|
|
||||||
|
@ -5541,7 +5541,7 @@ class MasterView(View):
|
||||||
input_file_templates=True,
|
input_file_templates=True,
|
||||||
output_file_templates=True):
|
output_file_templates=True):
|
||||||
app = self.get_rattail_app()
|
app = self.get_rattail_app()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
names = []
|
names = []
|
||||||
|
|
||||||
if simple_settings is None:
|
if simple_settings is None:
|
||||||
|
@ -6100,7 +6100,7 @@ class MasterView(View):
|
||||||
renderer='json')
|
renderer='json')
|
||||||
|
|
||||||
|
|
||||||
class ViewSupplement(object):
|
class ViewSupplement:
|
||||||
"""
|
"""
|
||||||
Base class for view "supplements" - which are sort of like plugins
|
Base class for view "supplements" - which are sort of like plugins
|
||||||
which can "supplement" certain aspects of the view.
|
which can "supplement" certain aspects of the view.
|
||||||
|
@ -6127,6 +6127,7 @@ class ViewSupplement(object):
|
||||||
def __init__(self, master):
|
def __init__(self, master):
|
||||||
self.master = master
|
self.master = master
|
||||||
self.request = master.request
|
self.request = master.request
|
||||||
|
self.app = master.app
|
||||||
self.model = master.model
|
self.model = master.model
|
||||||
self.rattail_config = master.rattail_config
|
self.rattail_config = master.rattail_config
|
||||||
self.Session = master.Session
|
self.Session = master.Session
|
||||||
|
@ -6160,7 +6161,7 @@ class ViewSupplement(object):
|
||||||
This is accomplished by subjecting the current base query to a
|
This is accomplished by subjecting the current base query to a
|
||||||
join, e.g. something like::
|
join, e.g. something like::
|
||||||
|
|
||||||
model = self.model
|
model = self.app.model
|
||||||
query = query.outerjoin(model.MyExtension)
|
query = query.outerjoin(model.MyExtension)
|
||||||
return query
|
return query
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -564,15 +564,19 @@ class PersonView(MasterView):
|
||||||
Method which must return the base query for the profile's POS
|
Method which must return the base query for the profile's POS
|
||||||
Transactions grid data.
|
Transactions grid data.
|
||||||
"""
|
"""
|
||||||
app = self.get_rattail_app()
|
customer = self.app.get_customer(person)
|
||||||
customer = app.get_customer(person)
|
|
||||||
|
|
||||||
key_field = app.get_customer_key_field()
|
if customer:
|
||||||
customer_key = getattr(customer, key_field)
|
key_field = self.app.get_customer_key_field()
|
||||||
if customer_key is not None:
|
customer_key = getattr(customer, key_field)
|
||||||
customer_key = str(customer_key)
|
if customer_key is not None:
|
||||||
|
customer_key = str(customer_key)
|
||||||
|
else:
|
||||||
|
# nb. this should *not* match anything, so query returns
|
||||||
|
# no results..
|
||||||
|
customer_key = person.uuid
|
||||||
|
|
||||||
trainwreck = app.get_trainwreck_handler()
|
trainwreck = self.app.get_trainwreck_handler()
|
||||||
model = trainwreck.get_model()
|
model = trainwreck.get_model()
|
||||||
query = TrainwreckSession.query(model.Transaction)\
|
query = TrainwreckSession.query(model.Transaction)\
|
||||||
.filter(model.Transaction.customer_id == customer_key)
|
.filter(model.Transaction.customer_id == customer_key)
|
||||||
|
|
|
@ -34,7 +34,7 @@ import sqlalchemy_continuum as continuum
|
||||||
|
|
||||||
from rattail import enum, pod, sil
|
from rattail import enum, pod, sil
|
||||||
from rattail.db import api, auth, Session as RattailSession
|
from rattail.db import api, auth, Session as RattailSession
|
||||||
from rattail.db.model import Product, PendingProduct, CustomerOrderItem
|
from rattail.db.model import Product, PendingProduct, ProductCost, CustomerOrderItem
|
||||||
from rattail.gpc import GPC
|
from rattail.gpc import GPC
|
||||||
from rattail.threads import Thread
|
from rattail.threads import Thread
|
||||||
from rattail.exceptions import LabelPrintingError
|
from rattail.exceptions import LabelPrintingError
|
||||||
|
@ -1857,7 +1857,8 @@ class ProductView(MasterView):
|
||||||
lookup_fields.append('alt_code')
|
lookup_fields.append('alt_code')
|
||||||
if lookup_fields:
|
if lookup_fields:
|
||||||
product = self.products_handler.locate_product_for_entry(
|
product = self.products_handler.locate_product_for_entry(
|
||||||
session, term, lookup_fields=lookup_fields)
|
session, term, lookup_fields=lookup_fields,
|
||||||
|
first_if_multiple=True)
|
||||||
if product:
|
if product:
|
||||||
final_results.append(self.search_normalize_result(product))
|
final_results.append(self.search_normalize_result(product))
|
||||||
|
|
||||||
|
@ -2668,6 +2669,78 @@ class PendingProductView(MasterView):
|
||||||
permission=f'{permission_prefix}.ignore_product')
|
permission=f'{permission_prefix}.ignore_product')
|
||||||
|
|
||||||
|
|
||||||
|
class ProductCostView(MasterView):
|
||||||
|
"""
|
||||||
|
Master view for Product Costs
|
||||||
|
"""
|
||||||
|
model_class = ProductCost
|
||||||
|
route_prefix = 'product_costs'
|
||||||
|
url_prefix = '/products/costs'
|
||||||
|
has_versions = True
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'_product_key_',
|
||||||
|
'vendor',
|
||||||
|
'preference',
|
||||||
|
'code',
|
||||||
|
'case_size',
|
||||||
|
'case_cost',
|
||||||
|
'pack_size',
|
||||||
|
'pack_cost',
|
||||||
|
'unit_cost',
|
||||||
|
]
|
||||||
|
|
||||||
|
def query(self, session):
|
||||||
|
""" """
|
||||||
|
query = super().query(session)
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# always join on Product
|
||||||
|
return query.join(model.Product)
|
||||||
|
|
||||||
|
def configure_grid(self, g):
|
||||||
|
""" """
|
||||||
|
super().configure_grid(g)
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# product key
|
||||||
|
field = self.get_product_key_field()
|
||||||
|
g.set_renderer(field, self.render_product_key)
|
||||||
|
g.set_sorter(field, getattr(model.Product, field))
|
||||||
|
g.set_sort_defaults(field)
|
||||||
|
g.set_filter(field, getattr(model.Product, field))
|
||||||
|
|
||||||
|
# vendor
|
||||||
|
g.set_joiner('vendor', lambda q: q.join(model.Vendor))
|
||||||
|
g.set_sorter('vendor', model.Vendor.name)
|
||||||
|
g.set_filter('vendor', model.Vendor.name, label="Vendor Name")
|
||||||
|
|
||||||
|
def render_product_key(self, cost, field):
|
||||||
|
""" """
|
||||||
|
handler = self.app.get_products_handler()
|
||||||
|
return handler.render_product_key(cost.product)
|
||||||
|
|
||||||
|
def configure_form(self, f):
|
||||||
|
""" """
|
||||||
|
super().configure_form(f)
|
||||||
|
|
||||||
|
# product
|
||||||
|
f.set_renderer('product', self.render_product)
|
||||||
|
if 'product_uuid' in f and 'product' in f:
|
||||||
|
f.remove('product')
|
||||||
|
f.replace('product_uuid', 'product')
|
||||||
|
|
||||||
|
# vendor
|
||||||
|
f.set_renderer('vendor', self.render_vendor)
|
||||||
|
if 'vendor_uuid' in f and 'vendor' in f:
|
||||||
|
f.remove('vendor')
|
||||||
|
f.replace('vendor_uuid', 'vendor')
|
||||||
|
|
||||||
|
# futures
|
||||||
|
# TODO: should eventually show a subgrid here?
|
||||||
|
f.remove('futures')
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
|
@ -2677,6 +2750,9 @@ def defaults(config, **kwargs):
|
||||||
PendingProductView = kwargs.get('PendingProductView', base['PendingProductView'])
|
PendingProductView = kwargs.get('PendingProductView', base['PendingProductView'])
|
||||||
PendingProductView.defaults(config)
|
PendingProductView.defaults(config)
|
||||||
|
|
||||||
|
ProductCostView = kwargs.get('ProductCostView', base['ProductCostView'])
|
||||||
|
ProductCostView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue