Compare commits
No commits in common. "master" and "v0.21.3" have entirely different histories.
34 changed files with 463 additions and 991 deletions
CHANGELOG.mdREADME.rst
docs
pyproject.tomltailbone
api
app.pydiffs.pyforms
grids
menus.pytemplates
base.makoconfigure.mako
datasync
deform
forms
master
ordering
products
receiving
reports/problems
themes/butterball
views
tests
114
CHANGELOG.md
114
CHANGELOG.md
|
@ -5,120 +5,6 @@ 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)
|
|
||||||
|
|
||||||
### Feat
|
|
||||||
|
|
||||||
- add support for new ordering batch from parsed file
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- avoid deprecated method to suggest username
|
|
||||||
|
|
||||||
## v0.21.11 (2024-10-03)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- custom method for adding grid action
|
|
||||||
- become/stop root should redirect to previous url
|
|
||||||
|
|
||||||
## v0.21.10 (2024-09-15)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- update project repo links, kallithea -> forgejo
|
|
||||||
- use better icon for submit button on login page
|
|
||||||
- wrap notes text for batch view
|
|
||||||
- expose datasync consumer batch size via configure page
|
|
||||||
|
|
||||||
## v0.21.9 (2024-08-28)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- render custom attrs in form component tag
|
|
||||||
|
|
||||||
## v0.21.8 (2024-08-28)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- ignore session kwarg for `MasterView.make_row_grid()`
|
|
||||||
|
|
||||||
## v0.21.7 (2024-08-28)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- avoid error when form value cannot be obtained
|
|
||||||
|
|
||||||
## v0.21.6 (2024-08-28)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- avoid error when grid value cannot be obtained
|
|
||||||
|
|
||||||
## v0.21.5 (2024-08-28)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- set empty string for "-new-" file configure option
|
|
||||||
|
|
||||||
## v0.21.4 (2024-08-26)
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
- handle differing email profile keys for appinfo/configure
|
|
||||||
|
|
||||||
## v0.21.3 (2024-08-26)
|
## v0.21.3 (2024-08-26)
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
|
||||||
# Tailbone
|
Tailbone
|
||||||
|
========
|
||||||
|
|
||||||
Tailbone is an extensible web application based on Rattail. It provides a
|
Tailbone is an extensible web application based on Rattail. It provides a
|
||||||
"back-office network environment" (BONE) for use in managing retail data.
|
"back-office network environment" (BONE) for use in managing retail data.
|
||||||
|
|
||||||
Please see Rattail's [home page](http://rattailproject.org/) for more
|
Please see Rattail's `home page`_ for more information.
|
||||||
information.
|
|
||||||
|
.. _home page: http://rattailproject.org/
|
|
@ -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://docs.wuttaproject.org/rattail/', None),
|
'rattail': ('https://rattailproject.org/docs/rattail/', None),
|
||||||
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
|
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
|
||||||
'wuttaweb': ('https://docs.wuttaproject.org/wuttaweb/', None),
|
'wuttaweb': ('https://rattailproject.org/docs/wuttaweb/', None),
|
||||||
'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None),
|
'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None),
|
||||||
}
|
}
|
||||||
|
|
||||||
# allow todo entries to show up
|
# allow todo entries to show up
|
||||||
|
|
|
@ -6,9 +6,9 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "Tailbone"
|
name = "Tailbone"
|
||||||
version = "0.22.7"
|
version = "0.21.3"
|
||||||
description = "Backoffice Web Application for Rattail"
|
description = "Backoffice Web Application for Rattail"
|
||||||
readme = "README.md"
|
readme = "README.rst"
|
||||||
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
||||||
license = {text = "GNU GPL v3+"}
|
license = {text = "GNU GPL v3+"}
|
||||||
classifiers = [
|
classifiers = [
|
||||||
|
@ -53,13 +53,13 @@ dependencies = [
|
||||||
"pyramid_mako",
|
"pyramid_mako",
|
||||||
"pyramid_retry",
|
"pyramid_retry",
|
||||||
"pyramid_tm",
|
"pyramid_tm",
|
||||||
"rattail[db,bouncer]>=0.20.1",
|
"rattail[db,bouncer]>=0.18.5",
|
||||||
"sa-filters",
|
"sa-filters",
|
||||||
"simplejson",
|
"simplejson",
|
||||||
"transaction",
|
"transaction",
|
||||||
"waitress",
|
"waitress",
|
||||||
"WebHelpers2",
|
"WebHelpers2",
|
||||||
"WuttaWeb>=0.21.0",
|
"WuttaWeb>=0.13.1",
|
||||||
"zope.sqlalchemy>=1.5",
|
"zope.sqlalchemy>=1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -84,9 +84,9 @@ tailbone = "tailbone.config:ConfigExtension"
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://rattailproject.org"
|
Homepage = "https://rattailproject.org"
|
||||||
Repository = "https://forgejo.wuttaproject.org/rattail/tailbone"
|
Repository = "https://kallithea.rattailproject.org/rattail-project/tailbone"
|
||||||
Issues = "https://forgejo.wuttaproject.org/rattail/tailbone/issues"
|
Issues = "https://redmine.rattailproject.org/projects/tailbone/issues"
|
||||||
Changelog = "https://forgejo.wuttaproject.org/rattail/tailbone/src/branch/master/CHANGELOG.md"
|
Changelog = "https://kallithea.rattailproject.org/rattail-project/tailbone/files/master/CHANGELOG.md"
|
||||||
|
|
||||||
|
|
||||||
[tool.commitizen]
|
[tool.commitizen]
|
||||||
|
|
|
@ -29,7 +29,8 @@ import logging
|
||||||
import humanize
|
import humanize
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from rattail.db.model import PurchaseBatch, PurchaseBatchRow
|
from rattail.db import model
|
||||||
|
from rattail.util import pretty_quantity
|
||||||
|
|
||||||
from cornice import Service
|
from cornice import Service
|
||||||
from deform import widget as dfwidget
|
from deform import widget as dfwidget
|
||||||
|
@ -44,7 +45,7 @@ log = logging.getLogger(__name__)
|
||||||
|
|
||||||
class ReceivingBatchViews(APIBatchView):
|
class ReceivingBatchViews(APIBatchView):
|
||||||
|
|
||||||
model_class = PurchaseBatch
|
model_class = model.PurchaseBatch
|
||||||
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
|
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
|
||||||
route_prefix = 'receivingbatchviews'
|
route_prefix = 'receivingbatchviews'
|
||||||
permission_prefix = 'receiving'
|
permission_prefix = 'receiving'
|
||||||
|
@ -54,8 +55,7 @@ class ReceivingBatchViews(APIBatchView):
|
||||||
supports_execute = True
|
supports_execute = True
|
||||||
|
|
||||||
def base_query(self):
|
def base_query(self):
|
||||||
model = self.app.model
|
query = super(ReceivingBatchViews, self).base_query()
|
||||||
query = super().base_query()
|
|
||||||
query = query.filter(model.PurchaseBatch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING)
|
query = query.filter(model.PurchaseBatch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING)
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ class ReceivingBatchViews(APIBatchView):
|
||||||
|
|
||||||
# assume "receive from PO" if given a PO key
|
# assume "receive from PO" if given a PO key
|
||||||
if data.get('purchase_key'):
|
if data.get('purchase_key'):
|
||||||
data['workflow'] = 'from_po'
|
data['receiving_workflow'] = 'from_po'
|
||||||
|
|
||||||
return super().create_object(data)
|
return super().create_object(data)
|
||||||
|
|
||||||
|
@ -120,7 +120,6 @@ class ReceivingBatchViews(APIBatchView):
|
||||||
return self._get(obj=batch)
|
return self._get(obj=batch)
|
||||||
|
|
||||||
def eligible_purchases(self):
|
def eligible_purchases(self):
|
||||||
model = self.app.model
|
|
||||||
uuid = self.request.params.get('vendor_uuid')
|
uuid = self.request.params.get('vendor_uuid')
|
||||||
vendor = self.Session.get(model.Vendor, uuid) if uuid else None
|
vendor = self.Session.get(model.Vendor, uuid) if uuid else None
|
||||||
if not vendor:
|
if not vendor:
|
||||||
|
@ -177,7 +176,7 @@ class ReceivingBatchViews(APIBatchView):
|
||||||
|
|
||||||
class ReceivingBatchRowViews(APIBatchRowView):
|
class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
|
|
||||||
model_class = PurchaseBatchRow
|
model_class = model.PurchaseBatchRow
|
||||||
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
|
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
|
||||||
route_prefix = 'receiving.rows'
|
route_prefix = 'receiving.rows'
|
||||||
permission_prefix = 'receiving'
|
permission_prefix = 'receiving'
|
||||||
|
@ -186,8 +185,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
supports_quick_entry = True
|
supports_quick_entry = True
|
||||||
|
|
||||||
def make_filter_spec(self):
|
def make_filter_spec(self):
|
||||||
model = self.app.model
|
filters = super(ReceivingBatchRowViews, self).make_filter_spec()
|
||||||
filters = super().make_filter_spec()
|
|
||||||
if filters:
|
if filters:
|
||||||
|
|
||||||
# must translate certain convenience filters
|
# must translate certain convenience filters
|
||||||
|
@ -298,11 +296,11 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
def normalize(self, row):
|
def normalize(self, row):
|
||||||
data = super().normalize(row)
|
data = super(ReceivingBatchRowViews, self).normalize(row)
|
||||||
model = self.app.model
|
|
||||||
|
|
||||||
batch = row.batch
|
batch = row.batch
|
||||||
prodder = self.app.get_products_handler()
|
app = self.get_rattail_app()
|
||||||
|
prodder = app.get_products_handler()
|
||||||
|
|
||||||
data['product_uuid'] = row.product_uuid
|
data['product_uuid'] = row.product_uuid
|
||||||
data['item_id'] = row.item_id
|
data['item_id'] = row.item_id
|
||||||
|
@ -377,7 +375,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
if accounted_for:
|
if accounted_for:
|
||||||
# some product accounted for; button should receive "remainder" only
|
# some product accounted for; button should receive "remainder" only
|
||||||
if remainder:
|
if remainder:
|
||||||
remainder = self.app.render_quantity(remainder)
|
remainder = pretty_quantity(remainder)
|
||||||
data['quick_receive_quantity'] = remainder
|
data['quick_receive_quantity'] = remainder
|
||||||
data['quick_receive_text'] = "Receive Remainder ({} {})".format(
|
data['quick_receive_text'] = "Receive Remainder ({} {})".format(
|
||||||
remainder, data['unit_uom'])
|
remainder, data['unit_uom'])
|
||||||
|
@ -388,7 +386,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
else: # nothing yet accounted for, button should receive "all"
|
else: # nothing yet accounted for, button should receive "all"
|
||||||
if not remainder:
|
if not remainder:
|
||||||
log.warning("quick receive remainder is empty for row %s", row.uuid)
|
log.warning("quick receive remainder is empty for row %s", row.uuid)
|
||||||
remainder = self.app.render_quantity(remainder)
|
remainder = pretty_quantity(remainder)
|
||||||
data['quick_receive_quantity'] = remainder
|
data['quick_receive_quantity'] = remainder
|
||||||
data['quick_receive_text'] = "Receive ALL ({} {})".format(
|
data['quick_receive_text'] = "Receive ALL ({} {})".format(
|
||||||
remainder, data['unit_uom'])
|
remainder, data['unit_uom'])
|
||||||
|
@ -416,7 +414,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
data['received_alert'] = None
|
data['received_alert'] = None
|
||||||
if self.batch_handler.get_units_confirmed(row):
|
if self.batch_handler.get_units_confirmed(row):
|
||||||
msg = "You have already received some of this product; last update was {}.".format(
|
msg = "You have already received some of this product; last update was {}.".format(
|
||||||
humanize.naturaltime(self.app.make_utc() - row.modified))
|
humanize.naturaltime(app.make_utc() - row.modified))
|
||||||
data['received_alert'] = msg
|
data['received_alert'] = msg
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
@ -425,8 +423,6 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
"""
|
"""
|
||||||
View which handles "receiving" against a particular batch row.
|
View which handles "receiving" against a particular batch row.
|
||||||
"""
|
"""
|
||||||
model = self.app.model
|
|
||||||
|
|
||||||
# first do basic input validation
|
# first do basic input validation
|
||||||
schema = ReceiveRow().bind(session=self.Session())
|
schema = ReceiveRow().bind(session=self.Session())
|
||||||
form = forms.Form(schema=schema, request=self.request)
|
form = forms.Form(schema=schema, request=self.request)
|
||||||
|
|
|
@ -26,6 +26,7 @@ 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
|
||||||
|
@ -184,7 +185,7 @@ class APIMasterView(APIView):
|
||||||
if sortcol:
|
if sortcol:
|
||||||
spec = {
|
spec = {
|
||||||
'field': sortcol.field_name,
|
'field': sortcol.field_name,
|
||||||
'direction': 'asc' if self.config.parse_bool(self.request.params['ascending']) else 'desc',
|
'direction': 'asc' if 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,17 +62,6 @@ 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-2024 Lance Edgar
|
# Copyright © 2010-2023 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -270,21 +270,9 @@ 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,
|
||||||
|
|
|
@ -401,8 +401,6 @@ class Form(object):
|
||||||
self.edit_help_url = edit_help_url
|
self.edit_help_url = edit_help_url
|
||||||
self.route_prefix = route_prefix
|
self.route_prefix = route_prefix
|
||||||
|
|
||||||
self.button_icon_submit = kwargs.get('button_icon_submit', 'save')
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(self.fields)
|
return iter(self.fields)
|
||||||
|
|
||||||
|
@ -1039,9 +1037,9 @@ class Form(object):
|
||||||
|
|
||||||
def render_vue_tag(self, **kwargs):
|
def render_vue_tag(self, **kwargs):
|
||||||
""" """
|
""" """
|
||||||
return self.render_vuejs_component(**kwargs)
|
return self.render_vuejs_component()
|
||||||
|
|
||||||
def render_vuejs_component(self, **kwargs):
|
def render_vuejs_component(self):
|
||||||
"""
|
"""
|
||||||
Render the Vue.js component HTML for the form.
|
Render the Vue.js component HTML for the form.
|
||||||
|
|
||||||
|
@ -1052,11 +1050,10 @@ class Form(object):
|
||||||
<tailbone-form :configure-fields-help="configureFieldsHelp">
|
<tailbone-form :configure-fields-help="configureFieldsHelp">
|
||||||
</tailbone-form>
|
</tailbone-form>
|
||||||
"""
|
"""
|
||||||
kw = dict(self.vuejs_component_kwargs)
|
kwargs = dict(self.vuejs_component_kwargs)
|
||||||
kw.update(kwargs)
|
|
||||||
if self.can_edit_help:
|
if self.can_edit_help:
|
||||||
kw.setdefault(':configure-fields-help', 'configureFieldsHelp')
|
kwargs.setdefault(':configure-fields-help', 'configureFieldsHelp')
|
||||||
return HTML.tag(self.vue_tagname, **kw)
|
return HTML.tag(self.vue_tagname, **kwargs)
|
||||||
|
|
||||||
def set_json_data(self, key, value):
|
def set_json_data(self, key, value):
|
||||||
"""
|
"""
|
||||||
|
@ -1383,11 +1380,7 @@ class Form(object):
|
||||||
return getattr(record, field_name)
|
return getattr(record, field_name)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
return record[field_name]
|
||||||
try:
|
|
||||||
return record[field_name]
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# TODO: is this always safe to do?
|
# TODO: is this always safe to do?
|
||||||
elif self.defaults and field_name in self.defaults:
|
elif self.defaults and field_name in self.defaults:
|
||||||
|
|
|
@ -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 paginated param instead",
|
"please use vue_tagname param instead",
|
||||||
DeprecationWarning, stacklevel=2)
|
DeprecationWarning, stacklevel=2)
|
||||||
kwargs.setdefault('paginated', kwargs.pop('pageable'))
|
kwargs.setdefault('paginated', kwargs.pop('pageable'))
|
||||||
|
|
||||||
|
@ -575,11 +575,7 @@ class Grid(WuttaGrid):
|
||||||
return getattr(obj, column_name)
|
return getattr(obj, column_name)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
return obj[column_name]
|
||||||
try:
|
|
||||||
return obj[column_name]
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def render_currency(self, obj, column_name):
|
def render_currency(self, obj, column_name):
|
||||||
value = self.obtain_value(obj, column_name)
|
value = self.obtain_value(obj, column_name)
|
||||||
|
@ -1223,7 +1219,6 @@ 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
|
||||||
|
@ -1240,10 +1235,7 @@ 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()
|
||||||
result = render(template, context)
|
return render(template, context)
|
||||||
if literal:
|
|
||||||
result = HTML.literal(result)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_view_click_handler(self):
|
def get_view_click_handler(self):
|
||||||
""" """
|
""" """
|
||||||
|
@ -1548,11 +1540,6 @@ class Grid(WuttaGrid):
|
||||||
self._table_data = results
|
self._table_data = results
|
||||||
return self._table_data
|
return self._table_data
|
||||||
|
|
||||||
# TODO: remove this when we use upstream GridAction
|
|
||||||
def add_action(self, key, **kwargs):
|
|
||||||
""" """
|
|
||||||
self.actions.append(GridAction(self.request, key, **kwargs))
|
|
||||||
|
|
||||||
def set_action_urls(self, row, rowobj, i):
|
def set_action_urls(self, row, rowobj, i):
|
||||||
"""
|
"""
|
||||||
Pre-generate all action URLs for the given data row. Meant for use
|
Pre-generate all action URLs for the given data row. Meant for use
|
||||||
|
|
|
@ -394,11 +394,6 @@ 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',
|
||||||
|
@ -456,11 +451,6 @@ 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",
|
||||||
|
|
|
@ -632,23 +632,9 @@
|
||||||
% endif
|
% endif
|
||||||
<div class="navbar-dropdown">
|
<div class="navbar-dropdown">
|
||||||
% if request.is_root:
|
% if request.is_root:
|
||||||
${h.form(url('stop_root'), ref='stopBeingRootForm')}
|
${h.link_to("Stop being root", url('stop_root'), class_='navbar-item root-user')}
|
||||||
${h.csrf_token(request)}
|
|
||||||
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
|
|
||||||
<a @click="$refs.stopBeingRootForm.submit()"
|
|
||||||
class="navbar-item root-user">
|
|
||||||
Stop being root
|
|
||||||
</a>
|
|
||||||
${h.end_form()}
|
|
||||||
% elif request.is_admin:
|
% elif request.is_admin:
|
||||||
${h.form(url('become_root'), ref='startBeingRootForm')}
|
${h.link_to("Become root", url('become_root'), class_='navbar-item root-user')}
|
||||||
${h.csrf_token(request)}
|
|
||||||
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
|
|
||||||
<a @click="$refs.startBeingRootForm.submit()"
|
|
||||||
class="navbar-item root-user">
|
|
||||||
Become root
|
|
||||||
</a>
|
|
||||||
${h.end_form()}
|
|
||||||
% endif
|
% endif
|
||||||
% if messaging_enabled:
|
% if messaging_enabled:
|
||||||
${h.link_to("Messages{}".format(" ({})".format(inbox_count) if inbox_count else ''), url('messages.inbox'), class_='navbar-item')}
|
${h.link_to("Messages{}".format(" ({})".format(inbox_count) if inbox_count else ''), url('messages.inbox'), class_='navbar-item')}
|
||||||
|
|
|
@ -280,14 +280,15 @@
|
||||||
<b-button @click="purgeSettingsShowDialog = false">
|
<b-button @click="purgeSettingsShowDialog = false">
|
||||||
Cancel
|
Cancel
|
||||||
</b-button>
|
</b-button>
|
||||||
${h.form(request.current_route_url(), **{'@submit': 'purgingSettings = true'})}
|
${h.form(request.current_route_url())}
|
||||||
${h.csrf_token(request)}
|
${h.csrf_token(request)}
|
||||||
${h.hidden('remove_settings', 'true')}
|
${h.hidden('remove_settings', 'true')}
|
||||||
<b-button type="is-danger"
|
<b-button type="is-danger"
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
:disabled="purgingSettings"
|
:disabled="purgingSettings"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="trash">
|
icon-left="trash"
|
||||||
|
@click="purgingSettings = true">
|
||||||
{{ purgingSettings ? "Working, please wait..." : "Remove All Settings" }}
|
{{ purgingSettings ? "Working, please wait..." : "Remove All Settings" }}
|
||||||
</b-button>
|
</b-button>
|
||||||
${h.end_form()}
|
${h.end_form()}
|
||||||
|
|
|
@ -83,8 +83,8 @@
|
||||||
</b-notification>
|
</b-notification>
|
||||||
|
|
||||||
<b-field>
|
<b-field>
|
||||||
<b-checkbox name="rattail.datasync.use_profile_settings"
|
<b-checkbox name="use_profile_settings"
|
||||||
v-model="simpleSettings['rattail.datasync.use_profile_settings']"
|
v-model="useProfileSettings"
|
||||||
native-value="true"
|
native-value="true"
|
||||||
@input="settingsNeedSaved = true">
|
@input="settingsNeedSaved = true">
|
||||||
Use these Settings to configure watchers and consumers
|
Use these Settings to configure watchers and consumers
|
||||||
|
@ -99,7 +99,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="level-right">
|
<div class="level-right">
|
||||||
<div class="level-item"
|
<div class="level-item"
|
||||||
v-show="simpleSettings['rattail.datasync.use_profile_settings']">
|
v-show="useProfileSettings">
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
@click="newProfile()"
|
@click="newProfile()"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
|
@ -162,7 +162,7 @@
|
||||||
</${b}-table-column>
|
</${b}-table-column>
|
||||||
<${b}-table-column label="Actions"
|
<${b}-table-column label="Actions"
|
||||||
v-slot="props"
|
v-slot="props"
|
||||||
v-if="simpleSettings['rattail.datasync.use_profile_settings']">
|
v-if="useProfileSettings">
|
||||||
<a href="#"
|
<a href="#"
|
||||||
class="grid-action"
|
class="grid-action"
|
||||||
@click.prevent="editProfile(props.row)">
|
@click.prevent="editProfile(props.row)">
|
||||||
|
@ -580,27 +580,18 @@
|
||||||
<b-field label="Supervisor Process Name"
|
<b-field label="Supervisor Process Name"
|
||||||
message="This should be the complete name, including group - e.g. poser:poser_datasync"
|
message="This should be the complete name, including group - e.g. poser:poser_datasync"
|
||||||
expanded>
|
expanded>
|
||||||
<b-input name="rattail.datasync.supervisor_process_name"
|
<b-input name="supervisor_process_name"
|
||||||
v-model="simpleSettings['rattail.datasync.supervisor_process_name']"
|
v-model="supervisorProcessName"
|
||||||
@input="settingsNeedSaved = true"
|
@input="settingsNeedSaved = true"
|
||||||
expanded>
|
expanded>
|
||||||
</b-input>
|
</b-input>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field label="Consumer Batch Size"
|
|
||||||
message="Max number of changes to be consumed at once."
|
|
||||||
expanded>
|
|
||||||
<numeric-input name="rattail.datasync.batch_size_limit"
|
|
||||||
v-model="simpleSettings['rattail.datasync.batch_size_limit']"
|
|
||||||
@input="settingsNeedSaved = true" />
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<h3 class="is-size-3">Legacy</h3>
|
|
||||||
<b-field label="Restart Command"
|
<b-field label="Restart Command"
|
||||||
message="This will run as '${system_user}' system user - please configure sudoers as needed. Typical command is like: sudo supervisorctl restart poser:poser_datasync"
|
message="This will run as '${system_user}' system user - please configure sudoers as needed. Typical command is like: sudo supervisorctl restart poser:poser_datasync"
|
||||||
expanded>
|
expanded>
|
||||||
<b-input name="tailbone.datasync.restart"
|
<b-input name="restart_command"
|
||||||
v-model="simpleSettings['tailbone.datasync.restart']"
|
v-model="restartCommand"
|
||||||
@input="settingsNeedSaved = true"
|
@input="settingsNeedSaved = true"
|
||||||
expanded>
|
expanded>
|
||||||
</b-input>
|
</b-input>
|
||||||
|
@ -615,6 +606,7 @@
|
||||||
ThisPageData.showConfigFilesNote = false
|
ThisPageData.showConfigFilesNote = false
|
||||||
ThisPageData.profilesData = ${json.dumps(profiles_data)|n}
|
ThisPageData.profilesData = ${json.dumps(profiles_data)|n}
|
||||||
ThisPageData.showDisabledProfiles = false
|
ThisPageData.showDisabledProfiles = false
|
||||||
|
ThisPageData.useProfileSettings = ${json.dumps(use_profile_settings)|n}
|
||||||
|
|
||||||
ThisPageData.editProfileShowDialog = false
|
ThisPageData.editProfileShowDialog = false
|
||||||
ThisPageData.editingProfile = null
|
ThisPageData.editingProfile = null
|
||||||
|
@ -639,6 +631,9 @@
|
||||||
ThisPageData.editingConsumerRunas = null
|
ThisPageData.editingConsumerRunas = null
|
||||||
ThisPageData.editingConsumerEnabled = true
|
ThisPageData.editingConsumerEnabled = true
|
||||||
|
|
||||||
|
ThisPageData.supervisorProcessName = ${json.dumps(supervisor_process_name)|n}
|
||||||
|
ThisPageData.restartCommand = ${json.dumps(restart_command)|n}
|
||||||
|
|
||||||
ThisPage.computed.updateConsumerDisabled = function() {
|
ThisPage.computed.updateConsumerDisabled = function() {
|
||||||
if (!this.editingConsumerKey) {
|
if (!this.editingConsumerKey) {
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<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;">
|
||||||
|
|
||||||
|
@ -9,7 +8,7 @@
|
||||||
${field.start_mapping()}
|
${field.start_mapping()}
|
||||||
<b-input type="password"
|
<b-input type="password"
|
||||||
name="${name}"
|
name="${name}"
|
||||||
v-model="${vmodel}"
|
value="${field.widget.redisplay and cstruct or ''}"
|
||||||
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|{};"
|
||||||
|
@ -19,6 +18,7 @@
|
||||||
</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|{};"
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
:disabled="${form.vue_component}Submitting"
|
:disabled="${form.vue_component}Submitting"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="${form.button_icon_submit}">
|
icon-left="save">
|
||||||
{{ ${form.vue_component}Submitting ? "Working, please wait..." : "${form.button_label_submit}" }}
|
{{ ${form.vue_component}Submitting ? "Working, please wait..." : "${form.button_label_submit}" }}
|
||||||
</b-button>
|
</b-button>
|
||||||
% else:
|
% else:
|
||||||
|
|
|
@ -196,7 +196,6 @@
|
||||||
|
|
||||||
<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"
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
## -*- coding: utf-8; -*-
|
|
||||||
<%inherit file="/configure.mako" />
|
|
||||||
|
|
||||||
<%def name="form_content()">
|
|
||||||
|
|
||||||
<h3 class="block is-size-3">Workflows</h3>
|
|
||||||
<div class="block" style="padding-left: 2rem;">
|
|
||||||
|
|
||||||
<p class="block">
|
|
||||||
Users can only choose from the workflows enabled below.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<b-field>
|
|
||||||
<b-checkbox name="rattail.batch.purchase.allow_ordering_from_scratch"
|
|
||||||
v-model="simpleSettings['rattail.batch.purchase.allow_ordering_from_scratch']"
|
|
||||||
native-value="true"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
From Scratch
|
|
||||||
</b-checkbox>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
<b-field>
|
|
||||||
<b-checkbox name="rattail.batch.purchase.allow_ordering_from_file"
|
|
||||||
v-model="simpleSettings['rattail.batch.purchase.allow_ordering_from_file']"
|
|
||||||
native-value="true"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
From Order File
|
|
||||||
</b-checkbox>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="block is-size-3">Vendors</h3>
|
|
||||||
<div class="block" style="padding-left: 2rem;">
|
|
||||||
|
|
||||||
<b-field message="If not set, user must choose a "supported" vendor.">
|
|
||||||
<b-checkbox name="rattail.batch.purchase.allow_ordering_any_vendor"
|
|
||||||
v-model="simpleSettings['rattail.batch.purchase.allow_ordering_any_vendor']"
|
|
||||||
native-value="true"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
Allow ordering for <span class="has-text-weight-bold">any</span> vendor
|
|
||||||
</b-checkbox>
|
|
||||||
</b-field>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="block is-size-3">Order Parsers</h3>
|
|
||||||
<div class="block" style="padding-left: 2rem;">
|
|
||||||
|
|
||||||
<p class="block">
|
|
||||||
Only the selected file parsers will be exposed to users.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
% for Parser in order_parsers:
|
|
||||||
<b-field message="${Parser.key}">
|
|
||||||
<b-checkbox name="order_parser_${Parser.key}"
|
|
||||||
v-model="orderParsers['${Parser.key}']"
|
|
||||||
native-value="true"
|
|
||||||
@input="settingsNeedSaved = true">
|
|
||||||
${Parser.title}
|
|
||||||
</b-checkbox>
|
|
||||||
</b-field>
|
|
||||||
% endfor
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="modify_vue_vars()">
|
|
||||||
${parent.modify_vue_vars()}
|
|
||||||
<script>
|
|
||||||
ThisPageData.orderParsers = ${json.dumps(order_parsers_data)|n}
|
|
||||||
</script>
|
|
||||||
</%def>
|
|
|
@ -55,20 +55,19 @@
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="render_form_template()">
|
<%def name="render_form_template()">
|
||||||
<script type="text/x-template" id="${form.vue_tagname}-template">
|
<script type="text/x-template" id="${form.component}-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.vue_tagname}-template',
|
template: '#${form.component}-template',
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
## TODO: deprecate / remove the latter option here
|
## TODO: deprecate / remove the latter option here
|
||||||
|
|
|
@ -69,12 +69,12 @@
|
||||||
<h3 class="block is-size-3">Vendors</h3>
|
<h3 class="block is-size-3">Vendors</h3>
|
||||||
<div class="block" style="padding-left: 2rem;">
|
<div class="block" style="padding-left: 2rem;">
|
||||||
|
|
||||||
<b-field message="If not set, user must choose a "supported" vendor.">
|
<b-field message="If set, user must choose a "supported" vendor; otherwise they may choose "any" vendor.">
|
||||||
<b-checkbox name="rattail.batch.purchase.allow_receiving_any_vendor"
|
<b-checkbox name="rattail.batch.purchase.supported_vendors_only"
|
||||||
v-model="simpleSettings['rattail.batch.purchase.allow_receiving_any_vendor']"
|
v-model="simpleSettings['rattail.batch.purchase.supported_vendors_only']"
|
||||||
native-value="true"
|
native-value="true"
|
||||||
@input="settingsNeedSaved = true">
|
@input="settingsNeedSaved = true">
|
||||||
Allow receiving for <span class="has-text-weight-bold">any</span> vendor
|
Only allow batch for "supported" vendors
|
||||||
</b-checkbox>
|
</b-checkbox>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
|
|
|
@ -45,10 +45,11 @@
|
||||||
<b-button @click="runReportShowDialog = false">
|
<b-button @click="runReportShowDialog = false">
|
||||||
Cancel
|
Cancel
|
||||||
</b-button>
|
</b-button>
|
||||||
${h.form(master.get_action_url('execute', instance), **{'@submit': 'runReportSubmitting = true'})}
|
${h.form(master.get_action_url('execute', instance))}
|
||||||
${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">
|
||||||
|
|
|
@ -909,7 +909,7 @@
|
||||||
${h.form(url('stop_root'), ref='stopBeingRootForm')}
|
${h.form(url('stop_root'), ref='stopBeingRootForm')}
|
||||||
${h.csrf_token(request)}
|
${h.csrf_token(request)}
|
||||||
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
|
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
|
||||||
<a @click="$refs.stopBeingRootForm.submit()"
|
<a @click="stopBeingRoot()"
|
||||||
class="navbar-item has-background-danger has-text-white">
|
class="navbar-item has-background-danger has-text-white">
|
||||||
Stop being root
|
Stop being root
|
||||||
</a>
|
</a>
|
||||||
|
@ -918,7 +918,7 @@
|
||||||
${h.form(url('become_root'), ref='startBeingRootForm')}
|
${h.form(url('become_root'), ref='startBeingRootForm')}
|
||||||
${h.csrf_token(request)}
|
${h.csrf_token(request)}
|
||||||
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
|
<input type="hidden" name="referrer" value="${request.current_route_url()}" />
|
||||||
<a @click="$refs.startBeingRootForm.submit()"
|
<a @click="startBeingRoot()"
|
||||||
class="navbar-item has-background-danger has-text-white">
|
class="navbar-item has-background-danger has-text-white">
|
||||||
Become root
|
Become root
|
||||||
</a>
|
</a>
|
||||||
|
@ -1103,6 +1103,18 @@
|
||||||
const key = 'menu_' + hash + '_shown'
|
const key = 'menu_' + hash + '_shown'
|
||||||
this[key] = !this[key]
|
this[key] = !this[key]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
% if request.is_admin:
|
||||||
|
|
||||||
|
startBeingRoot() {
|
||||||
|
this.$refs.startBeingRootForm.submit()
|
||||||
|
},
|
||||||
|
|
||||||
|
stopBeingRoot() {
|
||||||
|
this.$refs.stopBeingRootForm.submit()
|
||||||
|
},
|
||||||
|
|
||||||
|
% endif
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,8 @@
|
||||||
Auth Views
|
Auth Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from rattail.db.auth import set_user_password
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
from deform import widget as dfwidget
|
from deform import widget as dfwidget
|
||||||
from pyramid.httpexceptions import HTTPForbidden
|
from pyramid.httpexceptions import HTTPForbidden
|
||||||
|
@ -44,6 +46,28 @@ 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):
|
||||||
|
@ -80,7 +104,6 @@ class AuthenticationView(View):
|
||||||
form.save_label = "Login"
|
form.save_label = "Login"
|
||||||
form.show_reset = True
|
form.show_reset = True
|
||||||
form.show_cancel = False
|
form.show_cancel = False
|
||||||
form.button_icon_submit = 'user'
|
|
||||||
if form.validate():
|
if form.validate():
|
||||||
user = self.authenticate_user(form.validated['username'],
|
user = self.authenticate_user(form.validated['username'],
|
||||||
form.validated['password'])
|
form.validated['password'])
|
||||||
|
@ -94,6 +117,10 @@ 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()
|
||||||
|
@ -106,6 +133,7 @@ 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),
|
||||||
}
|
}
|
||||||
|
@ -154,27 +182,10 @@ class AuthenticationView(View):
|
||||||
self.request.user))
|
self.request.user))
|
||||||
return self.redirect(self.request.get_referrer())
|
return self.redirect(self.request.get_referrer())
|
||||||
|
|
||||||
def check_user_password(node, value):
|
schema = ChangePassword().bind(user=self.request.user, request=self.request)
|
||||||
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()
|
set_user_password(self.request.user, form.validated['new_password'])
|
||||||
auth.set_user_password(self.request.user, form.validated['new_password'])
|
|
||||||
self.request.session.flash("Your password has been changed.")
|
self.request.session.flash("Your password has been changed.")
|
||||||
return self.redirect(self.request.get_referrer())
|
return self.redirect(self.request.get_referrer())
|
||||||
|
|
||||||
|
|
|
@ -46,11 +46,10 @@ import colander
|
||||||
from deform import widget as dfwidget
|
from deform import widget as dfwidget
|
||||||
from webhelpers2.html import HTML, tags
|
from webhelpers2.html import HTML, tags
|
||||||
|
|
||||||
from wuttaweb.util import render_csrf_token
|
|
||||||
|
|
||||||
from tailbone import forms, grids
|
from tailbone import forms, grids
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView
|
||||||
|
from tailbone.util import csrf_token
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -384,7 +383,7 @@ class BatchMasterView(MasterView):
|
||||||
f.set_label('executed_by', "Executed by")
|
f.set_label('executed_by', "Executed by")
|
||||||
|
|
||||||
# notes
|
# notes
|
||||||
f.set_type('notes', 'text_wrapped')
|
f.set_type('notes', 'text')
|
||||||
|
|
||||||
# if self.creating and self.request.user:
|
# if self.creating and self.request.user:
|
||||||
# batch = fs.model
|
# batch = fs.model
|
||||||
|
@ -442,7 +441,7 @@ class BatchMasterView(MasterView):
|
||||||
|
|
||||||
form = [
|
form = [
|
||||||
begin_form,
|
begin_form,
|
||||||
render_csrf_token(self.request),
|
csrf_token(self.request),
|
||||||
tags.hidden('complete', value=value),
|
tags.hidden('complete', value=value),
|
||||||
submit,
|
submit,
|
||||||
tags.end_form(),
|
tags.end_form(),
|
||||||
|
|
|
@ -202,36 +202,10 @@ class DataSyncThreadView(MasterView):
|
||||||
return self.redirect(self.request.get_referrer(
|
return self.redirect(self.request.get_referrer(
|
||||||
default=self.request.route_url('datasyncchanges')))
|
default=self.request.route_url('datasyncchanges')))
|
||||||
|
|
||||||
def configure_get_simple_settings(self):
|
def configure_get_context(self):
|
||||||
""" """
|
|
||||||
return [
|
|
||||||
|
|
||||||
# basic
|
|
||||||
{'section': 'rattail.datasync',
|
|
||||||
'option': 'use_profile_settings',
|
|
||||||
'type': bool},
|
|
||||||
|
|
||||||
# misc.
|
|
||||||
{'section': 'rattail.datasync',
|
|
||||||
'option': 'supervisor_process_name'},
|
|
||||||
{'section': 'rattail.datasync',
|
|
||||||
'option': 'batch_size_limit',
|
|
||||||
'type': int},
|
|
||||||
|
|
||||||
# legacy
|
|
||||||
{'section': 'tailbone',
|
|
||||||
'option': 'datasync.restart'},
|
|
||||||
|
|
||||||
]
|
|
||||||
|
|
||||||
def configure_get_context(self, **kwargs):
|
|
||||||
""" """
|
|
||||||
context = super().configure_get_context(**kwargs)
|
|
||||||
|
|
||||||
profiles = self.datasync_handler.get_configured_profiles(
|
profiles = self.datasync_handler.get_configured_profiles(
|
||||||
include_disabled=True,
|
include_disabled=True,
|
||||||
ignore_problems=True)
|
ignore_problems=True)
|
||||||
context['profiles'] = profiles
|
|
||||||
|
|
||||||
profiles_data = []
|
profiles_data = []
|
||||||
for profile in sorted(profiles.values(), key=lambda p: p.key):
|
for profile in sorted(profiles.values(), key=lambda p: p.key):
|
||||||
|
@ -269,15 +243,25 @@ class DataSyncThreadView(MasterView):
|
||||||
data['consumers_data'] = consumers
|
data['consumers_data'] = consumers
|
||||||
profiles_data.append(data)
|
profiles_data.append(data)
|
||||||
|
|
||||||
context['profiles_data'] = profiles_data
|
return {
|
||||||
return context
|
'profiles': profiles,
|
||||||
|
'profiles_data': profiles_data,
|
||||||
|
'use_profile_settings': self.datasync_handler.should_use_profile_settings(),
|
||||||
|
'supervisor_process_name': self.rattail_config.get(
|
||||||
|
'rattail.datasync', 'supervisor_process_name'),
|
||||||
|
'restart_command': self.rattail_config.get(
|
||||||
|
'tailbone', 'datasync.restart'),
|
||||||
|
}
|
||||||
|
|
||||||
def configure_gather_settings(self, data, **kwargs):
|
def configure_gather_settings(self, data):
|
||||||
""" """
|
settings = []
|
||||||
settings = super().configure_gather_settings(data, **kwargs)
|
watch = []
|
||||||
|
|
||||||
if data.get('rattail.datasync.use_profile_settings') == 'true':
|
use_profile_settings = data.get('use_profile_settings') == 'true'
|
||||||
watch = []
|
settings.append({'name': 'rattail.datasync.use_profile_settings',
|
||||||
|
'value': 'true' if use_profile_settings else 'false'})
|
||||||
|
|
||||||
|
if use_profile_settings:
|
||||||
|
|
||||||
for profile in json.loads(data['profiles']):
|
for profile in json.loads(data['profiles']):
|
||||||
pkey = profile['key']
|
pkey = profile['key']
|
||||||
|
@ -339,12 +323,17 @@ class DataSyncThreadView(MasterView):
|
||||||
settings.append({'name': 'rattail.datasync.watch',
|
settings.append({'name': 'rattail.datasync.watch',
|
||||||
'value': ', '.join(watch)})
|
'value': ', '.join(watch)})
|
||||||
|
|
||||||
|
if data['supervisor_process_name']:
|
||||||
|
settings.append({'name': 'rattail.datasync.supervisor_process_name',
|
||||||
|
'value': data['supervisor_process_name']})
|
||||||
|
|
||||||
|
if data['restart_command']:
|
||||||
|
settings.append({'name': 'tailbone.datasync.restart',
|
||||||
|
'value': data['restart_command']})
|
||||||
|
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
def configure_remove_settings(self, **kwargs):
|
def configure_remove_settings(self):
|
||||||
""" """
|
|
||||||
super().configure_remove_settings(**kwargs)
|
|
||||||
|
|
||||||
purge_datasync_settings(self.rattail_config, self.Session())
|
purge_datasync_settings(self.rattail_config, self.Session())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -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.get_visible_data()
|
return grid.make_visible_data()
|
||||||
|
|
||||||
def get_grid_columns(self):
|
def get_grid_columns(self):
|
||||||
"""
|
"""
|
||||||
|
@ -551,8 +551,7 @@ class MasterView(View):
|
||||||
def get_quickie_result_url(self, obj):
|
def get_quickie_result_url(self, obj):
|
||||||
return self.get_action_url('view', obj)
|
return self.get_action_url('view', obj)
|
||||||
|
|
||||||
def make_row_grid(self, factory=None, key=None, data=None, columns=None,
|
def make_row_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
|
||||||
session=None, **kwargs):
|
|
||||||
"""
|
"""
|
||||||
Make and return a new (configured) rows grid instance.
|
Make and return a new (configured) rows grid instance.
|
||||||
"""
|
"""
|
||||||
|
@ -903,7 +902,7 @@ class MasterView(View):
|
||||||
|
|
||||||
def valid_employee_uuid(self, node, value):
|
def valid_employee_uuid(self, node, value):
|
||||||
if value:
|
if value:
|
||||||
model = self.app.model
|
model = self.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 +938,7 @@ class MasterView(View):
|
||||||
|
|
||||||
def valid_vendor_uuid(self, node, value):
|
def valid_vendor_uuid(self, node, value):
|
||||||
if value:
|
if value:
|
||||||
model = self.app.model
|
model = self.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 +1381,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.app.model
|
model = self.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 +1709,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.get_visible_data()
|
return grid.make_visible_data()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_row_url_prefix(cls):
|
def get_row_url_prefix(cls):
|
||||||
|
@ -2153,7 +2152,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.app.model
|
model = self.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 +2593,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.app.model
|
model = self.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 +2616,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.app.model
|
model = self.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 +2638,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.app.model
|
model = self.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
schema = colander.Schema()
|
schema = colander.Schema()
|
||||||
|
|
||||||
|
@ -2673,7 +2672,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.app.model
|
model = self.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
schema = colander.Schema()
|
schema = colander.Schema()
|
||||||
|
|
||||||
|
@ -5442,7 +5441,7 @@ class MasterView(View):
|
||||||
for template in self.normalize_input_file_templates(
|
for template in self.normalize_input_file_templates(
|
||||||
include_file_options=True):
|
include_file_options=True):
|
||||||
settings[template['setting_mode']] = template['mode']
|
settings[template['setting_mode']] = template['mode']
|
||||||
settings[template['setting_file']] = template['file'] or ''
|
settings[template['setting_file']] = template['file']
|
||||||
settings[template['setting_url']] = template['url']
|
settings[template['setting_url']] = template['url']
|
||||||
file_options[template['key']] = template['file_options']
|
file_options[template['key']] = template['file_options']
|
||||||
file_option_dirs[template['key']] = template['file_options_dir']
|
file_option_dirs[template['key']] = template['file_options_dir']
|
||||||
|
@ -5458,7 +5457,7 @@ class MasterView(View):
|
||||||
for template in self.normalize_output_file_templates(
|
for template in self.normalize_output_file_templates(
|
||||||
include_file_options=True):
|
include_file_options=True):
|
||||||
settings[template['setting_mode']] = template['mode']
|
settings[template['setting_mode']] = template['mode']
|
||||||
settings[template['setting_file']] = template['file'] or ''
|
settings[template['setting_file']] = template['file']
|
||||||
settings[template['setting_url']] = template['url']
|
settings[template['setting_url']] = template['url']
|
||||||
file_options[template['key']] = template['file_options']
|
file_options[template['key']] = template['file_options']
|
||||||
file_option_dirs[template['key']] = template['file_options_dir']
|
file_option_dirs[template['key']] = template['file_options_dir']
|
||||||
|
@ -5541,7 +5540,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.app.model
|
model = self.model
|
||||||
names = []
|
names = []
|
||||||
|
|
||||||
if simple_settings is None:
|
if simple_settings is None:
|
||||||
|
@ -6100,7 +6099,7 @@ class MasterView(View):
|
||||||
renderer='json')
|
renderer='json')
|
||||||
|
|
||||||
|
|
||||||
class ViewSupplement:
|
class ViewSupplement(object):
|
||||||
"""
|
"""
|
||||||
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,7 +6126,6 @@ class ViewSupplement:
|
||||||
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
|
||||||
|
@ -6161,7 +6159,7 @@ class ViewSupplement:
|
||||||
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.app.model
|
model = self.model
|
||||||
query = query.outerjoin(model.MyExtension)
|
query = query.outerjoin(model.MyExtension)
|
||||||
return query
|
return query
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -564,19 +564,15 @@ 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.
|
||||||
"""
|
"""
|
||||||
customer = self.app.get_customer(person)
|
app = self.get_rattail_app()
|
||||||
|
customer = app.get_customer(person)
|
||||||
|
|
||||||
if customer:
|
key_field = app.get_customer_key_field()
|
||||||
key_field = self.app.get_customer_key_field()
|
customer_key = getattr(customer, key_field)
|
||||||
customer_key = getattr(customer, key_field)
|
if customer_key is not None:
|
||||||
if customer_key is not None:
|
customer_key = str(customer_key)
|
||||||
customer_key = str(customer_key)
|
|
||||||
else:
|
|
||||||
# nb. this should *not* match anything, so query returns
|
|
||||||
# no results..
|
|
||||||
customer_key = person.uuid
|
|
||||||
|
|
||||||
trainwreck = self.app.get_trainwreck_handler()
|
trainwreck = 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)
|
||||||
|
@ -1386,8 +1382,8 @@ class PersonView(MasterView):
|
||||||
}
|
}
|
||||||
|
|
||||||
if not context['users']:
|
if not context['users']:
|
||||||
context['suggested_username'] = auth.make_unique_username(self.Session(),
|
context['suggested_username'] = auth.generate_unique_username(self.Session(),
|
||||||
person=person)
|
person=person)
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
|
@ -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, ProductCost, CustomerOrderItem
|
from rattail.db.model import Product, PendingProduct, 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,8 +1857,7 @@ 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))
|
||||||
|
|
||||||
|
@ -2669,78 +2668,6 @@ 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()
|
||||||
|
|
||||||
|
@ -2750,9 +2677,6 @@ 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)
|
||||||
|
|
|
@ -24,8 +24,6 @@
|
||||||
Base class for purchasing batch views
|
Base class for purchasing batch views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from rattail.db.model import PurchaseBatch, PurchaseBatchRow
|
from rattail.db.model import PurchaseBatch, PurchaseBatchRow
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
|
@ -69,8 +67,6 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
'store',
|
'store',
|
||||||
'buyer',
|
'buyer',
|
||||||
'vendor',
|
'vendor',
|
||||||
'description',
|
|
||||||
'workflow',
|
|
||||||
'department',
|
'department',
|
||||||
'purchase',
|
'purchase',
|
||||||
'vendor_email',
|
'vendor_email',
|
||||||
|
@ -162,174 +158,6 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
def batch_mode(self):
|
def batch_mode(self):
|
||||||
raise NotImplementedError("Please define `batch_mode` for your purchasing batch view")
|
raise NotImplementedError("Please define `batch_mode` for your purchasing batch view")
|
||||||
|
|
||||||
def get_supported_workflows(self):
|
|
||||||
"""
|
|
||||||
Return the supported "create batch" workflows.
|
|
||||||
"""
|
|
||||||
enum = self.app.enum
|
|
||||||
if self.batch_mode == enum.PURCHASE_BATCH_MODE_ORDERING:
|
|
||||||
return self.batch_handler.supported_ordering_workflows()
|
|
||||||
elif self.batch_mode == enum.PURCHASE_BATCH_MODE_RECEIVING:
|
|
||||||
return self.batch_handler.supported_receiving_workflows()
|
|
||||||
elif self.batch_mode == enum.PURCHASE_BATCH_MODE_COSTING:
|
|
||||||
return self.batch_handler.supported_costing_workflows()
|
|
||||||
raise ValueError("unknown batch mode")
|
|
||||||
|
|
||||||
def allow_any_vendor(self):
|
|
||||||
"""
|
|
||||||
Return boolean indicating whether creating a batch for "any"
|
|
||||||
vendor is allowed, vs. only supported vendors.
|
|
||||||
"""
|
|
||||||
enum = self.app.enum
|
|
||||||
|
|
||||||
if self.batch_mode == enum.PURCHASE_BATCH_MODE_ORDERING:
|
|
||||||
return self.batch_handler.allow_ordering_any_vendor()
|
|
||||||
|
|
||||||
elif self.batch_mode == enum.PURCHASE_BATCH_MODE_RECEIVING:
|
|
||||||
value = self.config.get_bool('rattail.batch.purchase.allow_receiving_any_vendor')
|
|
||||||
if value is not None:
|
|
||||||
return value
|
|
||||||
value = self.config.get_bool('rattail.batch.purchase.supported_vendors_only')
|
|
||||||
if value is not None:
|
|
||||||
warnings.warn("setting rattail.batch.purchase.supported_vendors_only is deprecated; "
|
|
||||||
"please use rattail.batch.purchase.allow_receiving_any_vendor instead",
|
|
||||||
DeprecationWarning)
|
|
||||||
# nb. must negate this setting
|
|
||||||
return not value
|
|
||||||
return False
|
|
||||||
|
|
||||||
raise ValueError("unknown batch mode")
|
|
||||||
|
|
||||||
def get_supported_vendors(self):
|
|
||||||
"""
|
|
||||||
Return the supported vendors for creating a batch.
|
|
||||||
"""
|
|
||||||
return []
|
|
||||||
|
|
||||||
def create(self, form=None, **kwargs):
|
|
||||||
"""
|
|
||||||
Custom view for creating a new batch. We split the process
|
|
||||||
into two steps, 1) choose workflow and 2) create batch. This
|
|
||||||
is because the specific form details for creating a batch will
|
|
||||||
depend on which "type" of batch creation is to be done, and
|
|
||||||
it's much easier to keep conditional logic for that in the
|
|
||||||
server instead of client-side etc.
|
|
||||||
"""
|
|
||||||
model = self.app.model
|
|
||||||
enum = self.app.enum
|
|
||||||
route_prefix = self.get_route_prefix()
|
|
||||||
|
|
||||||
workflows = self.get_supported_workflows()
|
|
||||||
valid_workflows = [workflow['workflow_key']
|
|
||||||
for workflow in workflows]
|
|
||||||
|
|
||||||
# if user has already identified their desired workflow, then
|
|
||||||
# we can just farm out to the default logic. we will of
|
|
||||||
# course configure our form differently, based on workflow,
|
|
||||||
# but this create() method at least will not need
|
|
||||||
# customization for that.
|
|
||||||
if self.request.matched_route.name.endswith('create_workflow'):
|
|
||||||
|
|
||||||
redirect = self.redirect(self.request.route_url(f'{route_prefix}.create'))
|
|
||||||
|
|
||||||
# however we do have one more thing to check - the workflow
|
|
||||||
# requested must of course be valid!
|
|
||||||
workflow_key = self.request.matchdict['workflow_key']
|
|
||||||
if workflow_key not in valid_workflows:
|
|
||||||
self.request.session.flash(f"Not a supported workflow: {workflow_key}", 'error')
|
|
||||||
raise redirect
|
|
||||||
|
|
||||||
# also, we require vendor to be correctly identified. if
|
|
||||||
# someone e.g. navigates to a URL by accident etc. we want
|
|
||||||
# to gracefully handle and redirect
|
|
||||||
uuid = self.request.matchdict['vendor_uuid']
|
|
||||||
vendor = self.Session.get(model.Vendor, uuid)
|
|
||||||
if not vendor:
|
|
||||||
self.request.session.flash("Invalid vendor selection. "
|
|
||||||
"Please choose an existing vendor.",
|
|
||||||
'warning')
|
|
||||||
raise redirect
|
|
||||||
|
|
||||||
# okay now do the normal thing, per workflow
|
|
||||||
return super().create(**kwargs)
|
|
||||||
|
|
||||||
# on the other hand, if caller provided a form, that means we are in
|
|
||||||
# the middle of some other custom workflow, e.g. "add child to truck
|
|
||||||
# dump parent" or some such. in which case we also defer to the normal
|
|
||||||
# logic, so as to not interfere with that.
|
|
||||||
if form:
|
|
||||||
return super().create(form=form, **kwargs)
|
|
||||||
|
|
||||||
# okay, at this point we need the user to select a vendor and workflow
|
|
||||||
self.creating = True
|
|
||||||
context = {}
|
|
||||||
|
|
||||||
# form to accept user choice of vendor/workflow
|
|
||||||
schema = colander.Schema()
|
|
||||||
schema.add(colander.SchemaNode(colander.String(), name='vendor'))
|
|
||||||
schema.add(colander.SchemaNode(colander.String(), name='workflow',
|
|
||||||
validator=colander.OneOf(valid_workflows)))
|
|
||||||
factory = self.get_form_factory()
|
|
||||||
form = factory(schema=schema, request=self.request)
|
|
||||||
|
|
||||||
# configure vendor field
|
|
||||||
vendor_handler = self.app.get_vendor_handler()
|
|
||||||
if self.allow_any_vendor():
|
|
||||||
# user may choose *any* available vendor
|
|
||||||
use_dropdown = vendor_handler.choice_uses_dropdown()
|
|
||||||
if use_dropdown:
|
|
||||||
vendors = self.Session.query(model.Vendor)\
|
|
||||||
.order_by(model.Vendor.id)\
|
|
||||||
.all()
|
|
||||||
vendor_values = [(vendor.uuid, f"({vendor.id}) {vendor.name}")
|
|
||||||
for vendor in vendors]
|
|
||||||
form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values))
|
|
||||||
if len(vendors) == 1:
|
|
||||||
form.set_default('vendor', vendors[0].uuid)
|
|
||||||
else:
|
|
||||||
vendor_display = ""
|
|
||||||
if self.request.method == 'POST':
|
|
||||||
if self.request.POST.get('vendor'):
|
|
||||||
vendor = self.Session.get(model.Vendor, self.request.POST['vendor'])
|
|
||||||
if vendor:
|
|
||||||
vendor_display = str(vendor)
|
|
||||||
vendors_url = self.request.route_url('vendors.autocomplete')
|
|
||||||
form.set_widget('vendor', forms.widgets.JQueryAutocompleteWidget(
|
|
||||||
field_display=vendor_display, service_url=vendors_url))
|
|
||||||
else: # only "supported" vendors allowed
|
|
||||||
vendors = self.get_supported_vendors()
|
|
||||||
vendor_values = [(vendor.uuid, vendor_handler.render_vendor(vendor))
|
|
||||||
for vendor in vendors]
|
|
||||||
form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values))
|
|
||||||
form.set_validator('vendor', self.valid_vendor_uuid)
|
|
||||||
|
|
||||||
# configure workflow field
|
|
||||||
values = [(workflow['workflow_key'], workflow['display'])
|
|
||||||
for workflow in workflows]
|
|
||||||
form.set_widget('workflow',
|
|
||||||
dfwidget.SelectWidget(values=values))
|
|
||||||
if len(workflows) == 1:
|
|
||||||
form.set_default('workflow', workflows[0]['workflow_key'])
|
|
||||||
|
|
||||||
form.submit_label = "Continue"
|
|
||||||
form.cancel_url = self.get_index_url()
|
|
||||||
|
|
||||||
# if form validates, that means user has chosen a creation
|
|
||||||
# type, so we just redirect to the appropriate "new batch of
|
|
||||||
# type X" page
|
|
||||||
if form.validate():
|
|
||||||
workflow_key = form.validated['workflow']
|
|
||||||
vendor_uuid = form.validated['vendor']
|
|
||||||
url = self.request.route_url(f'{route_prefix}.create_workflow',
|
|
||||||
workflow_key=workflow_key,
|
|
||||||
vendor_uuid=vendor_uuid)
|
|
||||||
raise self.redirect(url)
|
|
||||||
|
|
||||||
context['form'] = form
|
|
||||||
if hasattr(form, 'make_deform_form'):
|
|
||||||
context['dform'] = form.make_deform_form()
|
|
||||||
return self.render_to_response('create', context)
|
|
||||||
|
|
||||||
def query(self, session):
|
def query(self, session):
|
||||||
model = self.model
|
model = self.model
|
||||||
return session.query(model.PurchaseBatch)\
|
return session.query(model.PurchaseBatch)\
|
||||||
|
@ -398,40 +226,20 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
|
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
model = self.app.model
|
model = self.model
|
||||||
enum = self.app.enum
|
|
||||||
route_prefix = self.get_route_prefix()
|
|
||||||
|
|
||||||
today = self.app.today()
|
|
||||||
batch = f.model_instance
|
batch = f.model_instance
|
||||||
workflow = self.request.matchdict.get('workflow_key')
|
app = self.get_rattail_app()
|
||||||
vendor_handler = self.app.get_vendor_handler()
|
today = app.localtime().date()
|
||||||
|
|
||||||
# mode
|
# mode
|
||||||
f.set_enum('mode', enum.PURCHASE_BATCH_MODE)
|
f.set_enum('mode', self.enum.PURCHASE_BATCH_MODE)
|
||||||
|
|
||||||
# workflow
|
|
||||||
if self.creating:
|
|
||||||
if workflow:
|
|
||||||
f.set_widget('workflow', dfwidget.HiddenWidget())
|
|
||||||
f.set_default('workflow', workflow)
|
|
||||||
f.set_hidden('workflow')
|
|
||||||
# nb. show readonly '_workflow'
|
|
||||||
f.insert_after('workflow', '_workflow')
|
|
||||||
f.set_readonly('_workflow')
|
|
||||||
f.set_renderer('_workflow', self.render_workflow)
|
|
||||||
else:
|
|
||||||
f.set_readonly('workflow')
|
|
||||||
f.set_renderer('workflow', self.render_workflow)
|
|
||||||
else:
|
|
||||||
f.remove('workflow')
|
|
||||||
|
|
||||||
# store
|
# store
|
||||||
single_store = self.config.single_store()
|
single_store = self.rattail_config.single_store()
|
||||||
if self.creating:
|
if self.creating:
|
||||||
f.replace('store', 'store_uuid')
|
f.replace('store', 'store_uuid')
|
||||||
if single_store:
|
if single_store:
|
||||||
store = self.config.get_store(self.Session())
|
store = self.rattail_config.get_store(self.Session())
|
||||||
f.set_widget('store_uuid', dfwidget.HiddenWidget())
|
f.set_widget('store_uuid', dfwidget.HiddenWidget())
|
||||||
f.set_default('store_uuid', store.uuid)
|
f.set_default('store_uuid', store.uuid)
|
||||||
f.set_hidden('store_uuid')
|
f.set_hidden('store_uuid')
|
||||||
|
@ -455,6 +263,7 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
if self.creating:
|
if self.creating:
|
||||||
f.replace('vendor', 'vendor_uuid')
|
f.replace('vendor', 'vendor_uuid')
|
||||||
f.set_label('vendor_uuid', "Vendor")
|
f.set_label('vendor_uuid', "Vendor")
|
||||||
|
vendor_handler = app.get_vendor_handler()
|
||||||
use_dropdown = vendor_handler.choice_uses_dropdown()
|
use_dropdown = vendor_handler.choice_uses_dropdown()
|
||||||
if use_dropdown:
|
if use_dropdown:
|
||||||
vendors = self.Session.query(model.Vendor)\
|
vendors = self.Session.query(model.Vendor)\
|
||||||
|
@ -504,7 +313,7 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
if buyer:
|
if buyer:
|
||||||
buyer_display = str(buyer)
|
buyer_display = str(buyer)
|
||||||
elif self.creating:
|
elif self.creating:
|
||||||
buyer = self.app.get_employee(self.request.user)
|
buyer = app.get_employee(self.request.user)
|
||||||
if buyer:
|
if buyer:
|
||||||
buyer_display = str(buyer)
|
buyer_display = str(buyer)
|
||||||
f.set_default('buyer_uuid', buyer.uuid)
|
f.set_default('buyer_uuid', buyer.uuid)
|
||||||
|
@ -515,30 +324,6 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
field_display=buyer_display, service_url=buyers_url))
|
field_display=buyer_display, service_url=buyers_url))
|
||||||
f.set_label('buyer_uuid', "Buyer")
|
f.set_label('buyer_uuid', "Buyer")
|
||||||
|
|
||||||
# order_file
|
|
||||||
if self.creating:
|
|
||||||
f.set_type('order_file', 'file', required=False)
|
|
||||||
else:
|
|
||||||
f.set_readonly('order_file')
|
|
||||||
f.set_renderer('order_file', self.render_downloadable_file)
|
|
||||||
|
|
||||||
# order_parser_key
|
|
||||||
if self.creating:
|
|
||||||
kwargs = {}
|
|
||||||
if 'vendor_uuid' in self.request.matchdict:
|
|
||||||
vendor = self.Session.get(model.Vendor,
|
|
||||||
self.request.matchdict['vendor_uuid'])
|
|
||||||
if vendor:
|
|
||||||
kwargs['vendor'] = vendor
|
|
||||||
parsers = vendor_handler.get_supported_order_parsers(**kwargs)
|
|
||||||
parser_values = [(p.key, p.title) for p in parsers]
|
|
||||||
if len(parsers) == 1:
|
|
||||||
f.set_default('order_parser_key', parsers[0].key)
|
|
||||||
f.set_widget('order_parser_key', dfwidget.SelectWidget(values=parser_values))
|
|
||||||
f.set_label('order_parser_key', "Order Parser")
|
|
||||||
else:
|
|
||||||
f.remove_field('order_parser_key')
|
|
||||||
|
|
||||||
# invoice_file
|
# invoice_file
|
||||||
if self.creating:
|
if self.creating:
|
||||||
f.set_type('invoice_file', 'file', required=False)
|
f.set_type('invoice_file', 'file', required=False)
|
||||||
|
@ -556,7 +341,7 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
if vendor:
|
if vendor:
|
||||||
kwargs['vendor'] = vendor
|
kwargs['vendor'] = vendor
|
||||||
|
|
||||||
parsers = self.batch_handler.get_supported_invoice_parsers(**kwargs)
|
parsers = self.handler.get_supported_invoice_parsers(**kwargs)
|
||||||
parser_values = [(p.key, p.display) for p in parsers]
|
parser_values = [(p.key, p.display) for p in parsers]
|
||||||
if len(parsers) == 1:
|
if len(parsers) == 1:
|
||||||
f.set_default('invoice_parser_key', parsers[0].key)
|
f.set_default('invoice_parser_key', parsers[0].key)
|
||||||
|
@ -615,35 +400,6 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
'vendor_contact',
|
'vendor_contact',
|
||||||
'status_code')
|
'status_code')
|
||||||
|
|
||||||
# tweak some things if we are in "step 2" of creating new batch
|
|
||||||
if self.creating and workflow:
|
|
||||||
|
|
||||||
# display vendor but do not allow changing
|
|
||||||
vendor = self.Session.get(model.Vendor, self.request.matchdict['vendor_uuid'])
|
|
||||||
if not vendor:
|
|
||||||
raise ValueError(f"vendor not found: {self.request.matchdict['vendor_uuid']}")
|
|
||||||
f.set_readonly('vendor_uuid')
|
|
||||||
f.set_default('vendor_uuid', str(vendor))
|
|
||||||
|
|
||||||
# cancel should take us back to choosing a workflow
|
|
||||||
f.cancel_url = self.request.route_url(f'{route_prefix}.create')
|
|
||||||
|
|
||||||
def render_workflow(self, batch, field):
|
|
||||||
key = self.request.matchdict['workflow_key']
|
|
||||||
info = self.get_workflow_info(key)
|
|
||||||
if info:
|
|
||||||
return info['display']
|
|
||||||
|
|
||||||
def get_workflow_info(self, key):
|
|
||||||
enum = self.app.enum
|
|
||||||
if self.batch_mode == enum.PURCHASE_BATCH_MODE_ORDERING:
|
|
||||||
return self.batch_handler.ordering_workflow_info(key)
|
|
||||||
elif self.batch_mode == enum.PURCHASE_BATCH_MODE_RECEIVING:
|
|
||||||
return self.batch_handler.receiving_workflow_info(key)
|
|
||||||
elif self.batch_mode == enum.PURCHASE_BATCH_MODE_COSTING:
|
|
||||||
return self.batch_handler.costing_workflow_info(key)
|
|
||||||
raise ValueError("unknown batch mode")
|
|
||||||
|
|
||||||
def render_store(self, batch, field):
|
def render_store(self, batch, field):
|
||||||
store = batch.store
|
store = batch.store
|
||||||
if not store:
|
if not store:
|
||||||
|
@ -759,12 +515,10 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
|
|
||||||
def get_batch_kwargs(self, batch, **kwargs):
|
def get_batch_kwargs(self, batch, **kwargs):
|
||||||
kwargs = super().get_batch_kwargs(batch, **kwargs)
|
kwargs = super().get_batch_kwargs(batch, **kwargs)
|
||||||
model = self.app.model
|
model = self.model
|
||||||
|
|
||||||
kwargs['mode'] = self.batch_mode
|
kwargs['mode'] = self.batch_mode
|
||||||
kwargs['workflow'] = self.request.POST['workflow']
|
|
||||||
kwargs['truck_dump'] = batch.truck_dump
|
kwargs['truck_dump'] = batch.truck_dump
|
||||||
kwargs['order_parser_key'] = batch.order_parser_key
|
|
||||||
kwargs['invoice_parser_key'] = batch.invoice_parser_key
|
kwargs['invoice_parser_key'] = batch.invoice_parser_key
|
||||||
|
|
||||||
if batch.store:
|
if batch.store:
|
||||||
|
@ -782,11 +536,6 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
elif batch.vendor_uuid:
|
elif batch.vendor_uuid:
|
||||||
kwargs['vendor_uuid'] = batch.vendor_uuid
|
kwargs['vendor_uuid'] = batch.vendor_uuid
|
||||||
|
|
||||||
# must pull vendor from URL if it was not in form data
|
|
||||||
if 'vendor_uuid' not in kwargs and 'vendor' not in kwargs:
|
|
||||||
if 'vendor_uuid' in self.request.matchdict:
|
|
||||||
kwargs['vendor_uuid'] = self.request.matchdict['vendor_uuid']
|
|
||||||
|
|
||||||
if batch.department:
|
if batch.department:
|
||||||
kwargs['department'] = batch.department
|
kwargs['department'] = batch.department
|
||||||
elif batch.department_uuid:
|
elif batch.department_uuid:
|
||||||
|
@ -1170,25 +919,6 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
# # otherwise just view batch again
|
# # otherwise just view batch again
|
||||||
# return self.get_action_url('view', batch)
|
# return self.get_action_url('view', batch)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def defaults(cls, config):
|
|
||||||
cls._purchase_batch_defaults(config)
|
|
||||||
cls._batch_defaults(config)
|
|
||||||
cls._defaults(config)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _purchase_batch_defaults(cls, config):
|
|
||||||
route_prefix = cls.get_route_prefix()
|
|
||||||
url_prefix = cls.get_url_prefix()
|
|
||||||
permission_prefix = cls.get_permission_prefix()
|
|
||||||
|
|
||||||
# new batch using workflow X
|
|
||||||
config.add_route(f'{route_prefix}.create_workflow',
|
|
||||||
f'{url_prefix}/new/{{workflow_key}}/{{vendor_uuid}}')
|
|
||||||
config.add_view(cls, attr='create',
|
|
||||||
route_name=f'{route_prefix}.create_workflow',
|
|
||||||
permission=f'{permission_prefix}.create')
|
|
||||||
|
|
||||||
|
|
||||||
class NewProduct(colander.Schema):
|
class NewProduct(colander.Schema):
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2024 Lance Edgar
|
# Copyright © 2010-2023 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -28,10 +28,14 @@ import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import openpyxl
|
import openpyxl
|
||||||
|
from sqlalchemy import orm
|
||||||
|
|
||||||
|
from rattail.db import model, api
|
||||||
from rattail.core import Object
|
from rattail.core import Object
|
||||||
|
from rattail.time import localtime
|
||||||
|
|
||||||
|
from webhelpers2.html import tags
|
||||||
|
|
||||||
from tailbone.db import Session
|
|
||||||
from tailbone.views.purchasing import PurchasingBatchView
|
from tailbone.views.purchasing import PurchasingBatchView
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,8 +51,6 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
rows_editable = True
|
rows_editable = True
|
||||||
has_worksheet = True
|
has_worksheet = True
|
||||||
default_help_url = 'https://rattailproject.org/docs/rattail-manual/features/purchasing/ordering/index.html'
|
default_help_url = 'https://rattailproject.org/docs/rattail-manual/features/purchasing/ordering/index.html'
|
||||||
downloadable = True
|
|
||||||
configurable = True
|
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'po_total_calculated': "PO Total",
|
'po_total_calculated': "PO Total",
|
||||||
|
@ -57,14 +59,9 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
form_fields = [
|
form_fields = [
|
||||||
'id',
|
'id',
|
||||||
'store',
|
'store',
|
||||||
'vendor',
|
|
||||||
'description',
|
|
||||||
'workflow',
|
|
||||||
'order_file',
|
|
||||||
'order_parser_key',
|
|
||||||
'buyer',
|
'buyer',
|
||||||
|
'vendor',
|
||||||
'department',
|
'department',
|
||||||
'params',
|
|
||||||
'purchase',
|
'purchase',
|
||||||
'vendor_email',
|
'vendor_email',
|
||||||
'vendor_fax',
|
'vendor_fax',
|
||||||
|
@ -135,26 +132,15 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
return self.enum.PURCHASE_BATCH_MODE_ORDERING
|
return self.enum.PURCHASE_BATCH_MODE_ORDERING
|
||||||
|
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
super().configure_form(f)
|
super(OrderingBatchView, self).configure_form(f)
|
||||||
batch = f.model_instance
|
batch = f.model_instance
|
||||||
workflow = self.request.matchdict.get('workflow_key')
|
|
||||||
|
|
||||||
# purchase
|
# purchase
|
||||||
if self.creating or not batch.executed or not batch.purchase:
|
if self.creating or not batch.executed or not batch.purchase:
|
||||||
f.remove_field('purchase')
|
f.remove_field('purchase')
|
||||||
|
|
||||||
# now that all fields are setup, some final tweaks based on workflow
|
|
||||||
if self.creating and workflow:
|
|
||||||
|
|
||||||
if workflow == 'from_scratch':
|
|
||||||
f.remove('order_file',
|
|
||||||
'order_parser_key')
|
|
||||||
|
|
||||||
elif workflow == 'from_file':
|
|
||||||
f.set_required('order_file')
|
|
||||||
|
|
||||||
def get_batch_kwargs(self, batch, **kwargs):
|
def get_batch_kwargs(self, batch, **kwargs):
|
||||||
kwargs = super().get_batch_kwargs(batch, **kwargs)
|
kwargs = super(OrderingBatchView, self).get_batch_kwargs(batch, **kwargs)
|
||||||
kwargs['ship_method'] = batch.ship_method
|
kwargs['ship_method'] = batch.ship_method
|
||||||
kwargs['notes_to_vendor'] = batch.notes_to_vendor
|
kwargs['notes_to_vendor'] = batch.notes_to_vendor
|
||||||
return kwargs
|
return kwargs
|
||||||
|
@ -169,7 +155,7 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
* ``cases_ordered``
|
* ``cases_ordered``
|
||||||
* ``units_ordered``
|
* ``units_ordered``
|
||||||
"""
|
"""
|
||||||
super().configure_row_form(f)
|
super(OrderingBatchView, self).configure_row_form(f)
|
||||||
|
|
||||||
# when editing, only certain fields should allow changes
|
# when editing, only certain fields should allow changes
|
||||||
if self.editing:
|
if self.editing:
|
||||||
|
@ -322,7 +308,7 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
title = self.get_instance_title(batch)
|
title = self.get_instance_title(batch)
|
||||||
order_date = batch.date_ordered
|
order_date = batch.date_ordered
|
||||||
if not order_date:
|
if not order_date:
|
||||||
order_date = self.app.today()
|
order_date = localtime(self.rattail_config).date()
|
||||||
|
|
||||||
return self.render_to_response('worksheet', {
|
return self.render_to_response('worksheet', {
|
||||||
'batch': batch,
|
'batch': batch,
|
||||||
|
@ -383,7 +369,6 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
of being updated. If a matching row is not found, it will not be
|
of being updated. If a matching row is not found, it will not be
|
||||||
created.
|
created.
|
||||||
"""
|
"""
|
||||||
model = self.app.model
|
|
||||||
batch = self.get_instance()
|
batch = self.get_instance()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -493,75 +478,13 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
return self.file_response(path)
|
return self.file_response(path)
|
||||||
|
|
||||||
def get_execute_success_url(self, batch, result, **kwargs):
|
def get_execute_success_url(self, batch, result, **kwargs):
|
||||||
model = self.app.model
|
|
||||||
if isinstance(result, model.Purchase):
|
if isinstance(result, model.Purchase):
|
||||||
return self.request.route_url('purchases.view', uuid=result.uuid)
|
return self.request.route_url('purchases.view', uuid=result.uuid)
|
||||||
return super().get_execute_success_url(batch, result, **kwargs)
|
return super(OrderingBatchView, self).get_execute_success_url(batch, result, **kwargs)
|
||||||
|
|
||||||
def configure_get_simple_settings(self):
|
|
||||||
return [
|
|
||||||
|
|
||||||
# workflows
|
|
||||||
{'section': 'rattail.batch',
|
|
||||||
'option': 'purchase.allow_ordering_from_scratch',
|
|
||||||
'type': bool,
|
|
||||||
'default': True},
|
|
||||||
{'section': 'rattail.batch',
|
|
||||||
'option': 'purchase.allow_ordering_from_file',
|
|
||||||
'type': bool,
|
|
||||||
'default': True},
|
|
||||||
|
|
||||||
# vendors
|
|
||||||
{'section': 'rattail.batch',
|
|
||||||
'option': 'purchase.allow_ordering_any_vendor',
|
|
||||||
'type': bool,
|
|
||||||
'default': True,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
def configure_get_context(self):
|
|
||||||
context = super().configure_get_context()
|
|
||||||
vendor_handler = self.app.get_vendor_handler()
|
|
||||||
|
|
||||||
Parsers = vendor_handler.get_all_order_parsers()
|
|
||||||
Supported = vendor_handler.get_supported_order_parsers()
|
|
||||||
context['order_parsers'] = Parsers
|
|
||||||
context['order_parsers_data'] = dict([(Parser.key, Parser in Supported)
|
|
||||||
for Parser in Parsers])
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
def configure_gather_settings(self, data):
|
|
||||||
settings = super().configure_gather_settings(data)
|
|
||||||
vendor_handler = self.app.get_vendor_handler()
|
|
||||||
|
|
||||||
supported = []
|
|
||||||
for Parser in vendor_handler.get_all_order_parsers():
|
|
||||||
name = f'order_parser_{Parser.key}'
|
|
||||||
if data.get(name) == 'true':
|
|
||||||
supported.append(Parser.key)
|
|
||||||
settings.append({'name': 'rattail.vendors.supported_order_parsers',
|
|
||||||
'value': ', '.join(supported)})
|
|
||||||
|
|
||||||
return settings
|
|
||||||
|
|
||||||
def configure_remove_settings(self):
|
|
||||||
super().configure_remove_settings()
|
|
||||||
|
|
||||||
names = [
|
|
||||||
'rattail.vendors.supported_order_parsers',
|
|
||||||
]
|
|
||||||
|
|
||||||
# nb. using thread-local session here; we do not use
|
|
||||||
# self.Session b/c it may not point to Rattail
|
|
||||||
session = Session()
|
|
||||||
for name in names:
|
|
||||||
self.app.delete_setting(session, name)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config):
|
def defaults(cls, config):
|
||||||
cls._ordering_defaults(config)
|
cls._ordering_defaults(config)
|
||||||
cls._purchase_batch_defaults(config)
|
|
||||||
cls._batch_defaults(config)
|
cls._batch_defaults(config)
|
||||||
cls._defaults(config)
|
cls._defaults(config)
|
||||||
|
|
||||||
|
|
|
@ -108,7 +108,7 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
'store',
|
'store',
|
||||||
'vendor',
|
'vendor',
|
||||||
'description',
|
'description',
|
||||||
'workflow',
|
'receiving_workflow',
|
||||||
'truck_dump',
|
'truck_dump',
|
||||||
'truck_dump_children_first',
|
'truck_dump_children_first',
|
||||||
'truck_dump_children',
|
'truck_dump_children',
|
||||||
|
@ -235,18 +235,135 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
if not self.handler.allow_truck_dump_receiving():
|
if not self.handler.allow_truck_dump_receiving():
|
||||||
g.remove('truck_dump')
|
g.remove('truck_dump')
|
||||||
|
|
||||||
def get_supported_vendors(self):
|
def create(self, form=None, **kwargs):
|
||||||
""" """
|
"""
|
||||||
vendor_handler = self.app.get_vendor_handler()
|
Custom view for creating a new receiving batch. We split the process
|
||||||
vendors = {}
|
into two steps, 1) choose and 2) create. This is because the specific
|
||||||
for parser in self.batch_handler.get_supported_invoice_parsers():
|
form details for creating a batch will depend on which "type" of batch
|
||||||
if parser.vendor_key:
|
creation is to be done, and it's much easier to keep conditional logic
|
||||||
vendor = vendor_handler.get_vendor(self.Session(),
|
for that in the server instead of client-side etc.
|
||||||
parser.vendor_key)
|
|
||||||
if vendor:
|
See also
|
||||||
vendors[vendor.uuid] = vendor
|
:meth:`tailbone.views.purchasing.costing:CostingBatchView.create()`
|
||||||
vendors = sorted(vendors.values(), key=lambda v: v.name)
|
which uses similar logic.
|
||||||
return vendors
|
"""
|
||||||
|
model = self.model
|
||||||
|
route_prefix = self.get_route_prefix()
|
||||||
|
workflows = self.handler.supported_receiving_workflows()
|
||||||
|
valid_workflows = [workflow['workflow_key']
|
||||||
|
for workflow in workflows]
|
||||||
|
|
||||||
|
# if user has already identified their desired workflow, then we can
|
||||||
|
# just farm out to the default logic. we will of course configure our
|
||||||
|
# form differently, based on workflow, but this create() method at
|
||||||
|
# least will not need customization for that.
|
||||||
|
if self.request.matched_route.name.endswith('create_workflow'):
|
||||||
|
|
||||||
|
redirect = self.redirect(self.request.route_url('{}.create'.format(route_prefix)))
|
||||||
|
|
||||||
|
# however we do have one more thing to check - the workflow
|
||||||
|
# requested must of course be valid!
|
||||||
|
workflow_key = self.request.matchdict['workflow_key']
|
||||||
|
if workflow_key not in valid_workflows:
|
||||||
|
self.request.session.flash(
|
||||||
|
"Not a supported workflow: {}".format(workflow_key),
|
||||||
|
'error')
|
||||||
|
raise redirect
|
||||||
|
|
||||||
|
# also, we require vendor to be correctly identified. if
|
||||||
|
# someone e.g. navigates to a URL by accident etc. we want
|
||||||
|
# to gracefully handle and redirect
|
||||||
|
uuid = self.request.matchdict['vendor_uuid']
|
||||||
|
vendor = self.Session.get(model.Vendor, uuid)
|
||||||
|
if not vendor:
|
||||||
|
self.request.session.flash("Invalid vendor selection. "
|
||||||
|
"Please choose an existing vendor.",
|
||||||
|
'warning')
|
||||||
|
raise redirect
|
||||||
|
|
||||||
|
# okay now do the normal thing, per workflow
|
||||||
|
return super().create(**kwargs)
|
||||||
|
|
||||||
|
# on the other hand, if caller provided a form, that means we are in
|
||||||
|
# the middle of some other custom workflow, e.g. "add child to truck
|
||||||
|
# dump parent" or some such. in which case we also defer to the normal
|
||||||
|
# logic, so as to not interfere with that.
|
||||||
|
if form:
|
||||||
|
return super().create(form=form, **kwargs)
|
||||||
|
|
||||||
|
# okay, at this point we need the user to select a vendor and workflow
|
||||||
|
self.creating = True
|
||||||
|
context = {}
|
||||||
|
|
||||||
|
# form to accept user choice of vendor/workflow
|
||||||
|
schema = NewReceivingBatch().bind(valid_workflows=valid_workflows)
|
||||||
|
form = forms.Form(schema=schema, request=self.request)
|
||||||
|
|
||||||
|
# configure vendor field
|
||||||
|
app = self.get_rattail_app()
|
||||||
|
vendor_handler = app.get_vendor_handler()
|
||||||
|
if self.rattail_config.getbool('rattail.batch', 'purchase.supported_vendors_only'):
|
||||||
|
# only show vendors for which we have dedicated invoice parsers
|
||||||
|
vendors = {}
|
||||||
|
for parser in self.batch_handler.get_supported_invoice_parsers():
|
||||||
|
if parser.vendor_key:
|
||||||
|
vendor = vendor_handler.get_vendor(self.Session(),
|
||||||
|
parser.vendor_key)
|
||||||
|
if vendor:
|
||||||
|
vendors[vendor.uuid] = vendor
|
||||||
|
vendors = sorted(vendors.values(), key=lambda v: v.name)
|
||||||
|
vendor_values = [(vendor.uuid, vendor_handler.render_vendor(vendor))
|
||||||
|
for vendor in vendors]
|
||||||
|
form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values))
|
||||||
|
else:
|
||||||
|
# user may choose *any* available vendor
|
||||||
|
use_dropdown = vendor_handler.choice_uses_dropdown()
|
||||||
|
if use_dropdown:
|
||||||
|
vendors = self.Session.query(model.Vendor)\
|
||||||
|
.order_by(model.Vendor.id)\
|
||||||
|
.all()
|
||||||
|
vendor_values = [(vendor.uuid, "({}) {}".format(vendor.id, vendor.name))
|
||||||
|
for vendor in vendors]
|
||||||
|
form.set_widget('vendor', dfwidget.SelectWidget(values=vendor_values))
|
||||||
|
if len(vendors) == 1:
|
||||||
|
form.set_default('vendor', vendors[0].uuid)
|
||||||
|
else:
|
||||||
|
vendor_display = ""
|
||||||
|
if self.request.method == 'POST':
|
||||||
|
if self.request.POST.get('vendor'):
|
||||||
|
vendor = self.Session.get(model.Vendor, self.request.POST['vendor'])
|
||||||
|
if vendor:
|
||||||
|
vendor_display = str(vendor)
|
||||||
|
vendors_url = self.request.route_url('vendors.autocomplete')
|
||||||
|
form.set_widget('vendor', forms.widgets.JQueryAutocompleteWidget(
|
||||||
|
field_display=vendor_display, service_url=vendors_url))
|
||||||
|
form.set_validator('vendor', self.valid_vendor_uuid)
|
||||||
|
|
||||||
|
# configure workflow field
|
||||||
|
values = [(workflow['workflow_key'], workflow['display'])
|
||||||
|
for workflow in workflows]
|
||||||
|
form.set_widget('workflow',
|
||||||
|
dfwidget.SelectWidget(values=values))
|
||||||
|
if len(workflows) == 1:
|
||||||
|
form.set_default('workflow', workflows[0]['workflow_key'])
|
||||||
|
|
||||||
|
form.submit_label = "Continue"
|
||||||
|
form.cancel_url = self.get_index_url()
|
||||||
|
|
||||||
|
# if form validates, that means user has chosen a creation type, so we
|
||||||
|
# just redirect to the appropriate "new batch of type X" page
|
||||||
|
if form.validate():
|
||||||
|
workflow_key = form.validated['workflow']
|
||||||
|
vendor_uuid = form.validated['vendor']
|
||||||
|
url = self.request.route_url('{}.create_workflow'.format(route_prefix),
|
||||||
|
workflow_key=workflow_key,
|
||||||
|
vendor_uuid=vendor_uuid)
|
||||||
|
raise self.redirect(url)
|
||||||
|
|
||||||
|
context['form'] = form
|
||||||
|
if hasattr(form, 'make_deform_form'):
|
||||||
|
context['dform'] = form.make_deform_form()
|
||||||
|
return self.render_to_response('create', context)
|
||||||
|
|
||||||
def row_deletable(self, row):
|
def row_deletable(self, row):
|
||||||
|
|
||||||
|
@ -287,7 +404,13 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
# cancel should take us back to choosing a workflow
|
# cancel should take us back to choosing a workflow
|
||||||
f.cancel_url = self.request.route_url('{}.create'.format(route_prefix))
|
f.cancel_url = self.request.route_url('{}.create'.format(route_prefix))
|
||||||
|
|
||||||
# TODO: remove this
|
# receiving_workflow
|
||||||
|
if self.creating and workflow:
|
||||||
|
f.set_readonly('receiving_workflow')
|
||||||
|
f.set_renderer('receiving_workflow', self.render_receiving_workflow)
|
||||||
|
else:
|
||||||
|
f.remove('receiving_workflow')
|
||||||
|
|
||||||
# batch_type
|
# batch_type
|
||||||
if self.creating:
|
if self.creating:
|
||||||
f.set_widget('batch_type', dfwidget.HiddenWidget())
|
f.set_widget('batch_type', dfwidget.HiddenWidget())
|
||||||
|
@ -402,7 +525,7 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
|
|
||||||
# multiple invoice files (if applicable)
|
# multiple invoice files (if applicable)
|
||||||
if (not self.creating
|
if (not self.creating
|
||||||
and batch.get_param('workflow') == 'from_multi_invoice'):
|
and batch.get_param('receiving_workflow') == 'from_multi_invoice'):
|
||||||
|
|
||||||
if 'invoice_files' not in f:
|
if 'invoice_files' not in f:
|
||||||
f.insert_before('invoice_file', 'invoice_files')
|
f.insert_before('invoice_file', 'invoice_files')
|
||||||
|
@ -501,6 +624,12 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
items.append(HTML.tag('li', c=[link]))
|
items.append(HTML.tag('li', c=[link]))
|
||||||
return HTML.tag('ul', c=items)
|
return HTML.tag('ul', c=items)
|
||||||
|
|
||||||
|
def render_receiving_workflow(self, batch, field):
|
||||||
|
key = self.request.matchdict['workflow_key']
|
||||||
|
info = self.handler.receiving_workflow_info(key)
|
||||||
|
if info:
|
||||||
|
return info['display']
|
||||||
|
|
||||||
def get_visible_params(self, batch):
|
def get_visible_params(self, batch):
|
||||||
params = super().get_visible_params(batch)
|
params = super().get_visible_params(batch)
|
||||||
|
|
||||||
|
@ -525,40 +654,42 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
|
|
||||||
def get_batch_kwargs(self, batch, **kwargs):
|
def get_batch_kwargs(self, batch, **kwargs):
|
||||||
kwargs = super().get_batch_kwargs(batch, **kwargs)
|
kwargs = super().get_batch_kwargs(batch, **kwargs)
|
||||||
|
batch_type = self.request.POST['batch_type']
|
||||||
|
|
||||||
# must pull vendor from URL if it was not in form data
|
# must pull vendor from URL if it was not in form data
|
||||||
if 'vendor_uuid' not in kwargs and 'vendor' not in kwargs:
|
if 'vendor_uuid' not in kwargs and 'vendor' not in kwargs:
|
||||||
if 'vendor_uuid' in self.request.matchdict:
|
if 'vendor_uuid' in self.request.matchdict:
|
||||||
kwargs['vendor_uuid'] = self.request.matchdict['vendor_uuid']
|
kwargs['vendor_uuid'] = self.request.matchdict['vendor_uuid']
|
||||||
|
|
||||||
workflow = kwargs['workflow']
|
# TODO: ugh should just have workflow and no batch_type
|
||||||
if workflow == 'from_scratch':
|
kwargs['receiving_workflow'] = batch_type
|
||||||
|
if batch_type == 'from_scratch':
|
||||||
kwargs.pop('truck_dump_batch', None)
|
kwargs.pop('truck_dump_batch', None)
|
||||||
kwargs.pop('truck_dump_batch_uuid', None)
|
kwargs.pop('truck_dump_batch_uuid', None)
|
||||||
elif workflow == 'from_invoice':
|
elif batch_type == 'from_invoice':
|
||||||
pass
|
pass
|
||||||
elif workflow == 'from_multi_invoice':
|
elif batch_type == 'from_multi_invoice':
|
||||||
pass
|
pass
|
||||||
elif workflow == 'from_po':
|
elif batch_type == 'from_po':
|
||||||
# TODO: how to best handle this field? this doesn't seem flexible
|
# TODO: how to best handle this field? this doesn't seem flexible
|
||||||
kwargs['purchase_key'] = batch.purchase_uuid
|
kwargs['purchase_key'] = batch.purchase_uuid
|
||||||
elif workflow == 'from_po_with_invoice':
|
elif batch_type == 'from_po_with_invoice':
|
||||||
# TODO: how to best handle this field? this doesn't seem flexible
|
# TODO: how to best handle this field? this doesn't seem flexible
|
||||||
kwargs['purchase_key'] = batch.purchase_uuid
|
kwargs['purchase_key'] = batch.purchase_uuid
|
||||||
elif workflow == 'truck_dump_children_first':
|
elif batch_type == 'truck_dump_children_first':
|
||||||
kwargs['truck_dump'] = True
|
kwargs['truck_dump'] = True
|
||||||
kwargs['truck_dump_children_first'] = True
|
kwargs['truck_dump_children_first'] = True
|
||||||
kwargs['order_quantities_known'] = True
|
kwargs['order_quantities_known'] = True
|
||||||
# TODO: this makes sense in some cases, but all?
|
# TODO: this makes sense in some cases, but all?
|
||||||
# (should just omit that field when not relevant)
|
# (should just omit that field when not relevant)
|
||||||
kwargs['date_ordered'] = None
|
kwargs['date_ordered'] = None
|
||||||
elif workflow == 'truck_dump_children_last':
|
elif batch_type == 'truck_dump_children_last':
|
||||||
kwargs['truck_dump'] = True
|
kwargs['truck_dump'] = True
|
||||||
kwargs['truck_dump_ready'] = True
|
kwargs['truck_dump_ready'] = True
|
||||||
# TODO: this makes sense in some cases, but all?
|
# TODO: this makes sense in some cases, but all?
|
||||||
# (should just omit that field when not relevant)
|
# (should just omit that field when not relevant)
|
||||||
kwargs['date_ordered'] = None
|
kwargs['date_ordered'] = None
|
||||||
elif workflow.startswith('truck_dump_child'):
|
elif batch_type.startswith('truck_dump_child'):
|
||||||
truck_dump = self.get_instance()
|
truck_dump = self.get_instance()
|
||||||
kwargs['store'] = truck_dump.store
|
kwargs['store'] = truck_dump.store
|
||||||
kwargs['vendor'] = truck_dump.vendor
|
kwargs['vendor'] = truck_dump.vendor
|
||||||
|
@ -1855,12 +1986,6 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
'type': bool},
|
'type': bool},
|
||||||
|
|
||||||
# vendors
|
# vendors
|
||||||
{'section': 'rattail.batch',
|
|
||||||
'option': 'purchase.allow_receiving_any_vendor',
|
|
||||||
'type': bool},
|
|
||||||
# TODO: deprecated; can remove this once all live config
|
|
||||||
# is updated. but for now it remains so this setting is
|
|
||||||
# auto-deleted
|
|
||||||
{'section': 'rattail.batch',
|
{'section': 'rattail.batch',
|
||||||
'option': 'purchase.supported_vendors_only',
|
'option': 'purchase.supported_vendors_only',
|
||||||
'type': bool},
|
'type': bool},
|
||||||
|
@ -1911,7 +2036,6 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config):
|
def defaults(cls, config):
|
||||||
cls._receiving_defaults(config)
|
cls._receiving_defaults(config)
|
||||||
cls._purchase_batch_defaults(config)
|
|
||||||
cls._batch_defaults(config)
|
cls._batch_defaults(config)
|
||||||
cls._defaults(config)
|
cls._defaults(config)
|
||||||
|
|
||||||
|
@ -1919,11 +2043,17 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
def _receiving_defaults(cls, config):
|
def _receiving_defaults(cls, config):
|
||||||
rattail_config = config.registry.settings.get('rattail_config')
|
rattail_config = config.registry.settings.get('rattail_config')
|
||||||
route_prefix = cls.get_route_prefix()
|
route_prefix = cls.get_route_prefix()
|
||||||
|
url_prefix = cls.get_url_prefix()
|
||||||
instance_url_prefix = cls.get_instance_url_prefix()
|
instance_url_prefix = cls.get_instance_url_prefix()
|
||||||
model_key = cls.get_model_key()
|
model_key = cls.get_model_key()
|
||||||
model_title = cls.get_model_title()
|
model_title = cls.get_model_title()
|
||||||
permission_prefix = cls.get_permission_prefix()
|
permission_prefix = cls.get_permission_prefix()
|
||||||
|
|
||||||
|
# new receiving batch using workflow X
|
||||||
|
config.add_route('{}.create_workflow'.format(route_prefix), '{}/new/{{workflow_key}}/{{vendor_uuid}}'.format(url_prefix))
|
||||||
|
config.add_view(cls, attr='create', route_name='{}.create_workflow'.format(route_prefix),
|
||||||
|
permission='{}.create'.format(permission_prefix))
|
||||||
|
|
||||||
# row-level receiving
|
# row-level receiving
|
||||||
config.add_route('{}.receive_row'.format(route_prefix), '{}/rows/{{row_uuid}}/receive'.format(instance_url_prefix))
|
config.add_route('{}.receive_row'.format(route_prefix), '{}/rows/{{row_uuid}}/receive'.format(instance_url_prefix))
|
||||||
config.add_view(cls, attr='receive_row', route_name='{}.receive_row'.format(route_prefix),
|
config.add_view(cls, attr='receive_row', route_name='{}.receive_row'.format(route_prefix),
|
||||||
|
@ -1976,6 +2106,33 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
permission='{}.auto_receive'.format(permission_prefix))
|
permission='{}.auto_receive'.format(permission_prefix))
|
||||||
|
|
||||||
|
|
||||||
|
@colander.deferred
|
||||||
|
def valid_workflow(node, kw):
|
||||||
|
"""
|
||||||
|
Deferred validator for ``workflow`` field, for new batches.
|
||||||
|
"""
|
||||||
|
valid_workflows = kw['valid_workflows']
|
||||||
|
|
||||||
|
def validate(node, value):
|
||||||
|
# we just need to provide possible values, and let stock validator
|
||||||
|
# handle the rest
|
||||||
|
oneof = colander.OneOf(valid_workflows)
|
||||||
|
return oneof(node, value)
|
||||||
|
|
||||||
|
return validate
|
||||||
|
|
||||||
|
|
||||||
|
class NewReceivingBatch(colander.Schema):
|
||||||
|
"""
|
||||||
|
Schema for choosing which "type" of new receiving batch should be created.
|
||||||
|
"""
|
||||||
|
vendor = colander.SchemaNode(colander.String(),
|
||||||
|
label="Vendor")
|
||||||
|
|
||||||
|
workflow = colander.SchemaNode(colander.String(),
|
||||||
|
validator=valid_workflow)
|
||||||
|
|
||||||
|
|
||||||
class ReceiveRowForm(colander.MappingSchema):
|
class ReceiveRowForm(colander.MappingSchema):
|
||||||
|
|
||||||
mode = colander.SchemaNode(colander.String(),
|
mode = colander.SchemaNode(colander.String(),
|
||||||
|
|
|
@ -77,41 +77,13 @@ class AppInfoView(WuttaAppInfoView):
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
# nb. these email settings require special handling below
|
|
||||||
configure_profile_key_mismatches = [
|
|
||||||
'default.subject',
|
|
||||||
'default.to',
|
|
||||||
'default.cc',
|
|
||||||
'default.bcc',
|
|
||||||
'feedback.subject',
|
|
||||||
'feedback.to',
|
|
||||||
]
|
|
||||||
|
|
||||||
def configure_get_simple_settings(self):
|
def configure_get_simple_settings(self):
|
||||||
""" """
|
""" """
|
||||||
simple_settings = super().configure_get_simple_settings()
|
simple_settings = super().configure_get_simple_settings()
|
||||||
|
|
||||||
# TODO:
|
|
||||||
# there are several email config keys which differ between
|
|
||||||
# wuttjamaican and rattail. basically all of the "profile" keys
|
|
||||||
# have a different prefix.
|
|
||||||
|
|
||||||
# after wuttaweb has declared its settings, we examine each and
|
|
||||||
# overwrite the value if one is defined with rattail config key.
|
|
||||||
# (nb. this happens even if wuttjamaican key has a value!)
|
|
||||||
|
|
||||||
# note that we *do* declare the profile mismatch keys for
|
|
||||||
# rattail, as part of simple settings. this ensures the
|
|
||||||
# parent logic will always remove them when saving. however
|
|
||||||
# we must also include them in gather_settings() to ensure
|
|
||||||
# they are saved to match wuttjamaican values.
|
|
||||||
|
|
||||||
# there are also a couple of flags where rattail's default is the
|
|
||||||
# opposite of wuttjamaican. so we overwrite those too as needed.
|
|
||||||
|
|
||||||
for setting in simple_settings:
|
for setting in simple_settings:
|
||||||
|
|
||||||
# nb. the update home page redirect setting is off by
|
# TODO: the update home page redirect setting is off by
|
||||||
# default for wuttaweb, but on for tailbone
|
# default for wuttaweb, but on for tailbone
|
||||||
if setting['name'] == 'wuttaweb.home_redirect_to_login':
|
if setting['name'] == 'wuttaweb.home_redirect_to_login':
|
||||||
value = self.config.get_bool('wuttaweb.home_redirect_to_login')
|
value = self.config.get_bool('wuttaweb.home_redirect_to_login')
|
||||||
|
@ -119,43 +91,55 @@ class AppInfoView(WuttaAppInfoView):
|
||||||
value = self.config.get_bool('tailbone.login_is_home', default=True)
|
value = self.config.get_bool('tailbone.login_is_home', default=True)
|
||||||
setting['value'] = value
|
setting['value'] = value
|
||||||
|
|
||||||
# nb. sending email is off by default for wuttjamaican,
|
# TODO: sending email is off by default for wuttjamaican,
|
||||||
# but on for rattail
|
# but on for rattail
|
||||||
elif setting['name'] == 'rattail.mail.send_emails':
|
elif setting['name'] == 'rattail.mail.send_emails':
|
||||||
value = self.config.get_bool('rattail.mail.send_emails', default=True)
|
value = self.config.get_bool('rattail.mail.send_emails', default=True)
|
||||||
setting['value'] = value
|
setting['value'] = value
|
||||||
|
|
||||||
# nb. this one is even more special, key is entirely different
|
# TODO: email defaults have different config keys in rattail
|
||||||
elif setting['name'] == 'rattail.email.default.sender':
|
elif setting['name'] == 'rattail.email.default.sender':
|
||||||
value = self.config.get('rattail.email.default.sender')
|
value = self.config.get('rattail.email.default.sender')
|
||||||
if value is None:
|
if value is None:
|
||||||
value = self.config.get('rattail.mail.default.from')
|
value = self.config.get('rattail.mail.default.from')
|
||||||
setting['value'] = value
|
setting['value'] = value
|
||||||
|
|
||||||
else:
|
# TODO: email defaults have different config keys in rattail
|
||||||
|
elif setting['name'] == 'rattail.email.default.subject':
|
||||||
|
value = self.config.get('rattail.email.default.subject')
|
||||||
|
if value is None:
|
||||||
|
value = self.config.get('rattail.mail.default.subject')
|
||||||
|
setting['value'] = value
|
||||||
|
|
||||||
# nb. fetch alternate value for profile key mismatch
|
# TODO: email defaults have different config keys in rattail
|
||||||
for key in self.configure_profile_key_mismatches:
|
elif setting['name'] == 'rattail.email.default.to':
|
||||||
if setting['name'] == f'rattail.email.{key}':
|
value = self.config.get('rattail.email.default.to')
|
||||||
value = self.config.get(f'rattail.email.{key}')
|
if value is None:
|
||||||
if value is None:
|
value = self.config.get('rattail.mail.default.to')
|
||||||
value = self.config.get(f'rattail.mail.{key}')
|
setting['value'] = value
|
||||||
setting['value'] = value
|
|
||||||
break
|
# TODO: email defaults have different config keys in rattail
|
||||||
|
elif setting['name'] == 'rattail.email.default.cc':
|
||||||
|
value = self.config.get('rattail.email.default.cc')
|
||||||
|
if value is None:
|
||||||
|
value = self.config.get('rattail.mail.default.cc')
|
||||||
|
setting['value'] = value
|
||||||
|
|
||||||
|
# TODO: email defaults have different config keys in rattail
|
||||||
|
elif setting['name'] == 'rattail.email.default.bcc':
|
||||||
|
value = self.config.get('rattail.email.default.bcc')
|
||||||
|
if value is None:
|
||||||
|
value = self.config.get('rattail.mail.default.bcc')
|
||||||
|
setting['value'] = value
|
||||||
|
|
||||||
# nb. these are no longer used (deprecated), but we keep
|
# nb. these are no longer used (deprecated), but we keep
|
||||||
# them defined here so the tool auto-deletes them
|
# them defined here so the tool auto-deletes them
|
||||||
|
|
||||||
simple_settings.extend([
|
simple_settings.extend([
|
||||||
{'name': 'tailbone.login_is_home'},
|
|
||||||
{'name': 'tailbone.buefy_version'},
|
{'name': 'tailbone.buefy_version'},
|
||||||
{'name': 'tailbone.vue_version'},
|
{'name': 'tailbone.vue_version'},
|
||||||
])
|
])
|
||||||
|
|
||||||
simple_settings.append({'name': 'rattail.mail.default.from'})
|
|
||||||
for key in self.configure_profile_key_mismatches:
|
|
||||||
simple_settings.append({'name': f'rattail.mail.{key}'})
|
|
||||||
|
|
||||||
for key in self.get_weblibs():
|
for key in self.get_weblibs():
|
||||||
simple_settings.extend([
|
simple_settings.extend([
|
||||||
{'name': f'tailbone.libver.{key}'},
|
{'name': f'tailbone.libver.{key}'},
|
||||||
|
@ -164,28 +148,6 @@ class AppInfoView(WuttaAppInfoView):
|
||||||
|
|
||||||
return simple_settings
|
return simple_settings
|
||||||
|
|
||||||
def configure_gather_settings(self, data, simple_settings=None):
|
|
||||||
""" """
|
|
||||||
settings = super().configure_gather_settings(data, simple_settings=simple_settings)
|
|
||||||
|
|
||||||
# nb. must add legacy rattail profile settings to match new ones
|
|
||||||
for setting in list(settings):
|
|
||||||
|
|
||||||
if setting['name'] == 'rattail.email.default.sender':
|
|
||||||
value = setting['value']
|
|
||||||
settings.append({'name': 'rattail.mail.default.from',
|
|
||||||
'value': value})
|
|
||||||
|
|
||||||
else:
|
|
||||||
for key in self.configure_profile_key_mismatches:
|
|
||||||
if setting['name'] == f'rattail.email.{key}':
|
|
||||||
value = setting['value']
|
|
||||||
settings.append({'name': f'rattail.mail.{key}',
|
|
||||||
'value': value})
|
|
||||||
break
|
|
||||||
|
|
||||||
return settings
|
|
||||||
|
|
||||||
|
|
||||||
class SettingView(MasterView):
|
class SettingView(MasterView):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -348,27 +348,56 @@ class UpgradeView(MasterView):
|
||||||
commit_hash_pattern = re.compile(r'^.{40}$')
|
commit_hash_pattern = re.compile(r'^.{40}$')
|
||||||
|
|
||||||
def get_changelog_projects(self):
|
def get_changelog_projects(self):
|
||||||
project_map = {
|
projects = {
|
||||||
'onager': 'onager',
|
'rattail': {
|
||||||
'pyCOREPOS': 'pycorepos',
|
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/rattail/changelog/{new_version}/?size=10',
|
||||||
'rattail': 'rattail',
|
'release_url': 'https://kallithea.rattailproject.org/rattail-project/rattail/files/v{new_version}/CHANGES.rst',
|
||||||
'rattail_corepos': 'rattail-corepos',
|
},
|
||||||
'rattail-onager': 'rattail-onager',
|
'Tailbone': {
|
||||||
'rattail_tempmon': 'rattail-tempmon',
|
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone/changelog/{new_version}/?size=10',
|
||||||
'rattail_woocommerce': 'rattail-woocommerce',
|
'release_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone/files/v{new_version}/CHANGES.rst',
|
||||||
'Tailbone': 'tailbone',
|
},
|
||||||
'tailbone_corepos': 'tailbone-corepos',
|
'pyCOREPOS': {
|
||||||
'tailbone-onager': 'tailbone-onager',
|
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/pycorepos/changelog/{new_version}/?size=10',
|
||||||
'tailbone_theo': 'theo',
|
'release_url': 'https://kallithea.rattailproject.org/rattail-project/pycorepos/files/v{new_version}/CHANGES.rst',
|
||||||
'tailbone_woocommerce': 'tailbone-woocommerce',
|
},
|
||||||
|
'rattail_corepos': {
|
||||||
|
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-corepos/changelog/{new_version}/?size=10',
|
||||||
|
'release_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-corepos/files/v{new_version}/CHANGES.rst',
|
||||||
|
},
|
||||||
|
'tailbone_corepos': {
|
||||||
|
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone-corepos/changelog/{new_version}/?size=10',
|
||||||
|
'release_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone-corepos/files/v{new_version}/CHANGES.rst',
|
||||||
|
},
|
||||||
|
'onager': {
|
||||||
|
'commit_url': 'https://kallithea.rattailproject.org/rattail-restricted/onager/changelog/{new_version}/?size=10',
|
||||||
|
'release_url': 'https://kallithea.rattailproject.org/rattail-restricted/onager/files/v{new_version}/CHANGES.rst',
|
||||||
|
},
|
||||||
|
'rattail-onager': {
|
||||||
|
'commit_url': 'https://kallithea.rattailproject.org/rattail-restricted/rattail-onager/changelog/{new_version}/?size=10',
|
||||||
|
'release_url': 'https://kallithea.rattailproject.org/rattail-restricted/rattail-onager/files/v{new_version}/CHANGELOG.md',
|
||||||
|
},
|
||||||
|
'rattail_tempmon': {
|
||||||
|
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-tempmon/changelog/{new_version}/?size=10',
|
||||||
|
'release_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-tempmon/files/v{new_version}/CHANGES.rst',
|
||||||
|
},
|
||||||
|
'tailbone-onager': {
|
||||||
|
'commit_url': 'https://kallithea.rattailproject.org/rattail-restricted/tailbone-onager/changelog/{new_version}/?size=10',
|
||||||
|
'release_url': 'https://kallithea.rattailproject.org/rattail-restricted/tailbone-onager/files/v{new_version}/CHANGELOG.md',
|
||||||
|
},
|
||||||
|
'rattail_woocommerce': {
|
||||||
|
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-woocommerce/changelog/{new_version}/?size=10',
|
||||||
|
'release_url': 'https://kallithea.rattailproject.org/rattail-project/rattail-woocommerce/files/v{new_version}/CHANGES.rst',
|
||||||
|
},
|
||||||
|
'tailbone_woocommerce': {
|
||||||
|
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone-woocommerce/changelog/{new_version}/?size=10',
|
||||||
|
'release_url': 'https://kallithea.rattailproject.org/rattail-project/tailbone-woocommerce/files/v{new_version}/CHANGES.rst',
|
||||||
|
},
|
||||||
|
'tailbone_theo': {
|
||||||
|
'commit_url': 'https://kallithea.rattailproject.org/rattail-project/theo/changelog/{new_version}/?size=10',
|
||||||
|
'release_url': 'https://kallithea.rattailproject.org/rattail-project/theo/files/v{new_version}/CHANGES.rst',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
projects = {}
|
|
||||||
for name, repo in project_map.items():
|
|
||||||
projects[name] = {
|
|
||||||
'commit_url': f'https://forgejo.wuttaproject.org/rattail/{repo}/compare/{{old_version}}...{{new_version}}',
|
|
||||||
'release_url': f'https://forgejo.wuttaproject.org/rattail/{repo}/src/tag/v{{new_version}}/CHANGELOG.md',
|
|
||||||
}
|
|
||||||
return projects
|
return projects
|
||||||
|
|
||||||
def get_changelog_url(self, project, old_version, new_version):
|
def get_changelog_url(self, project, old_version, new_version):
|
||||||
|
|
|
@ -5,9 +5,12 @@ from unittest import TestCase
|
||||||
|
|
||||||
from pyramid.config import Configurator
|
from pyramid.config import Configurator
|
||||||
|
|
||||||
|
from wuttjamaican.testing import FileConfigTestCase
|
||||||
|
|
||||||
from rattail.exceptions import ConfigurationError
|
from rattail.exceptions import ConfigurationError
|
||||||
from rattail.testing import DataTestCase
|
from rattail.config import RattailConfig
|
||||||
from tailbone import app as mod
|
from tailbone import app as mod
|
||||||
|
from tests.util import DataTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestRattailConfig(TestCase):
|
class TestRattailConfig(TestCase):
|
||||||
|
@ -27,7 +30,7 @@ class TestRattailConfig(TestCase):
|
||||||
|
|
||||||
class TestMakePyramidConfig(DataTestCase):
|
class TestMakePyramidConfig(DataTestCase):
|
||||||
|
|
||||||
def make_config(self, **kwargs):
|
def make_config(self):
|
||||||
myconf = self.write_file('web.conf', """
|
myconf = self.write_file('web.conf', """
|
||||||
[rattail.db]
|
[rattail.db]
|
||||||
default.url = sqlite://
|
default.url = sqlite://
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue