Compare commits
32 commits
Author | SHA1 | Date | |
---|---|---|---|
e150453801 | |||
e2582ffec5 | |||
a6508154cb | |||
7348eec671 | |||
4221fa50dd | |||
e0ebd43e7a | |||
c7ee9de9eb | |||
950db697a0 | |||
358b3b75a5 | |||
7e559a01b3 | |||
23bdde245a | |||
2c269b640b | |||
![]() |
f1c8ffedda | ||
![]() |
aace6033c5 | ||
![]() |
7171c7fb06 | ||
![]() |
993f066f2c | ||
![]() |
980031f524 | ||
![]() |
bcaf0d08bc | ||
![]() |
ac439c949b | ||
![]() |
20b3f87dbe | ||
![]() |
9e55717041 | ||
![]() |
772b6610cb | ||
![]() |
3f27f626df | ||
![]() |
29743e70b7 | ||
![]() |
54220601ed | ||
![]() |
9a6f8970ae | ||
![]() |
28f90ad6b5 | ||
![]() |
535317e4f7 | ||
![]() |
072db39233 | ||
![]() |
c6365f2631 | ||
![]() |
d520f64fee | ||
![]() |
2308d2e240 |
25 changed files with 766 additions and 314 deletions
69
CHANGELOG.md
69
CHANGELOG.md
|
@ -5,6 +5,75 @@ 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)
|
## v0.21.10 (2024-09-15)
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|
|
@ -27,10 +27,10 @@ templates_path = ['_templates']
|
||||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||||
|
|
||||||
intersphinx_mapping = {
|
intersphinx_mapping = {
|
||||||
'rattail': ('https://rattailproject.org/docs/rattail/', None),
|
'rattail': ('https://docs.wuttaproject.org/rattail/', None),
|
||||||
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
|
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
|
||||||
'wuttaweb': ('https://rattailproject.org/docs/wuttaweb/', None),
|
'wuttaweb': ('https://docs.wuttaproject.org/wuttaweb/', None),
|
||||||
'wuttjamaican': ('https://rattailproject.org/docs/wuttjamaican/', None),
|
'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None),
|
||||||
}
|
}
|
||||||
|
|
||||||
# allow todo entries to show up
|
# allow todo entries to show up
|
||||||
|
|
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "Tailbone"
|
name = "Tailbone"
|
||||||
version = "0.21.10"
|
version = "0.22.7"
|
||||||
description = "Backoffice Web Application for Rattail"
|
description = "Backoffice Web Application for Rattail"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
||||||
|
@ -53,13 +53,13 @@ dependencies = [
|
||||||
"pyramid_mako",
|
"pyramid_mako",
|
||||||
"pyramid_retry",
|
"pyramid_retry",
|
||||||
"pyramid_tm",
|
"pyramid_tm",
|
||||||
"rattail[db,bouncer]>=0.18.5",
|
"rattail[db,bouncer]>=0.20.1",
|
||||||
"sa-filters",
|
"sa-filters",
|
||||||
"simplejson",
|
"simplejson",
|
||||||
"transaction",
|
"transaction",
|
||||||
"waitress",
|
"waitress",
|
||||||
"WebHelpers2",
|
"WebHelpers2",
|
||||||
"WuttaWeb>=0.14.0",
|
"WuttaWeb>=0.21.0",
|
||||||
"zope.sqlalchemy>=1.5",
|
"zope.sqlalchemy>=1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -29,8 +29,7 @@ import logging
|
||||||
import humanize
|
import humanize
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db.model import PurchaseBatch, PurchaseBatchRow
|
||||||
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
|
||||||
|
@ -45,7 +44,7 @@ log = logging.getLogger(__name__)
|
||||||
|
|
||||||
class ReceivingBatchViews(APIBatchView):
|
class ReceivingBatchViews(APIBatchView):
|
||||||
|
|
||||||
model_class = model.PurchaseBatch
|
model_class = 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'
|
||||||
|
@ -55,7 +54,8 @@ class ReceivingBatchViews(APIBatchView):
|
||||||
supports_execute = True
|
supports_execute = True
|
||||||
|
|
||||||
def base_query(self):
|
def base_query(self):
|
||||||
query = super(ReceivingBatchViews, self).base_query()
|
model = self.app.model
|
||||||
|
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['receiving_workflow'] = 'from_po'
|
data['workflow'] = 'from_po'
|
||||||
|
|
||||||
return super().create_object(data)
|
return super().create_object(data)
|
||||||
|
|
||||||
|
@ -120,6 +120,7 @@ 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:
|
||||||
|
@ -176,7 +177,7 @@ class ReceivingBatchViews(APIBatchView):
|
||||||
|
|
||||||
class ReceivingBatchRowViews(APIBatchRowView):
|
class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
|
|
||||||
model_class = model.PurchaseBatchRow
|
model_class = 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'
|
||||||
|
@ -185,7 +186,8 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
supports_quick_entry = True
|
supports_quick_entry = True
|
||||||
|
|
||||||
def make_filter_spec(self):
|
def make_filter_spec(self):
|
||||||
filters = super(ReceivingBatchRowViews, self).make_filter_spec()
|
model = self.app.model
|
||||||
|
filters = super().make_filter_spec()
|
||||||
if filters:
|
if filters:
|
||||||
|
|
||||||
# must translate certain convenience filters
|
# must translate certain convenience filters
|
||||||
|
@ -296,11 +298,11 @@ class ReceivingBatchRowViews(APIBatchRowView):
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
def normalize(self, row):
|
def normalize(self, row):
|
||||||
data = super(ReceivingBatchRowViews, self).normalize(row)
|
data = super().normalize(row)
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
batch = row.batch
|
batch = row.batch
|
||||||
app = self.get_rattail_app()
|
prodder = self.app.get_products_handler()
|
||||||
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
|
||||||
|
@ -375,7 +377,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 = pretty_quantity(remainder)
|
remainder = self.app.render_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'])
|
||||||
|
@ -386,7 +388,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 = pretty_quantity(remainder)
|
remainder = self.app.render_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'])
|
||||||
|
@ -414,7 +416,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(app.make_utc() - row.modified))
|
humanize.naturaltime(self.app.make_utc() - row.modified))
|
||||||
data['received_alert'] = msg
|
data['received_alert'] = msg
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
@ -423,6 +425,8 @@ 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,7 +26,6 @@ Tailbone Web API - Master View
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from rattail.config import parse_bool
|
|
||||||
from rattail.db.util import get_fieldnames
|
from rattail.db.util import get_fieldnames
|
||||||
|
|
||||||
from cornice import resource, Service
|
from cornice import resource, Service
|
||||||
|
@ -185,7 +184,7 @@ class APIMasterView(APIView):
|
||||||
if sortcol:
|
if sortcol:
|
||||||
spec = {
|
spec = {
|
||||||
'field': sortcol.field_name,
|
'field': sortcol.field_name,
|
||||||
'direction': 'asc' if parse_bool(self.request.params['ascending']) else 'desc',
|
'direction': 'asc' if self.config.parse_bool(self.request.params['ascending']) else 'desc',
|
||||||
}
|
}
|
||||||
if sortcol.model_name:
|
if sortcol.model_name:
|
||||||
spec['model'] = sortcol.model_name
|
spec['model'] = sortcol.model_name
|
||||||
|
|
|
@ -62,6 +62,17 @@ def make_rattail_config(settings):
|
||||||
# nb. this is for compaibility with wuttaweb
|
# nb. this is for compaibility with wuttaweb
|
||||||
settings['wutta_config'] = rattail_config
|
settings['wutta_config'] = rattail_config
|
||||||
|
|
||||||
|
# must import all sqlalchemy models before things get rolling,
|
||||||
|
# otherwise can have errors about continuum TransactionMeta class
|
||||||
|
# not yet mapped, when relevant pages are first requested...
|
||||||
|
# cf. https://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/database/sqlalchemy.html#importing-all-sqlalchemy-models
|
||||||
|
# hat tip to https://stackoverflow.com/a/59241485
|
||||||
|
if getattr(rattail_config, 'tempmon_engine', None):
|
||||||
|
from rattail_tempmon.db import model as tempmon_model, Session as TempmonSession
|
||||||
|
tempmon_session = TempmonSession()
|
||||||
|
tempmon_session.query(tempmon_model.Appliance).first()
|
||||||
|
tempmon_session.close()
|
||||||
|
|
||||||
# configure database sessions
|
# configure database sessions
|
||||||
if hasattr(rattail_config, 'appdb_engine'):
|
if hasattr(rattail_config, 'appdb_engine'):
|
||||||
tailbone.db.Session.configure(bind=rattail_config.appdb_engine)
|
tailbone.db.Session.configure(bind=rattail_config.appdb_engine)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -270,9 +270,21 @@ class VersionDiff(Diff):
|
||||||
for field in self.fields:
|
for field in self.fields:
|
||||||
values[field] = {'before': self.render_old_value(field),
|
values[field] = {'before': self.render_old_value(field),
|
||||||
'after': self.render_new_value(field)}
|
'after': self.render_new_value(field)}
|
||||||
|
|
||||||
|
operation = None
|
||||||
|
if self.version.operation_type == continuum.Operation.INSERT:
|
||||||
|
operation = 'INSERT'
|
||||||
|
elif self.version.operation_type == continuum.Operation.UPDATE:
|
||||||
|
operation = 'UPDATE'
|
||||||
|
elif self.version.operation_type == continuum.Operation.DELETE:
|
||||||
|
operation = 'DELETE'
|
||||||
|
else:
|
||||||
|
operation = self.version.operation_type
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'key': id(self.version),
|
'key': id(self.version),
|
||||||
'model_title': self.title,
|
'model_title': self.title,
|
||||||
|
'operation': operation,
|
||||||
'diff_class': self.nature,
|
'diff_class': self.nature,
|
||||||
'fields': self.fields,
|
'fields': self.fields,
|
||||||
'values': values,
|
'values': values,
|
||||||
|
|
|
@ -235,7 +235,7 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
if 'pageable' in kwargs:
|
if 'pageable' in kwargs:
|
||||||
warnings.warn("pageable param is deprecated for Grid(); "
|
warnings.warn("pageable param is deprecated for Grid(); "
|
||||||
"please use vue_tagname param instead",
|
"please use paginated param instead",
|
||||||
DeprecationWarning, stacklevel=2)
|
DeprecationWarning, stacklevel=2)
|
||||||
kwargs.setdefault('paginated', kwargs.pop('pageable'))
|
kwargs.setdefault('paginated', kwargs.pop('pageable'))
|
||||||
|
|
||||||
|
@ -1223,6 +1223,7 @@ class Grid(WuttaGrid):
|
||||||
|
|
||||||
def render_table_element(self, template='/grids/b-table.mako',
|
def render_table_element(self, template='/grids/b-table.mako',
|
||||||
data_prop='gridData', empty_labels=False,
|
data_prop='gridData', empty_labels=False,
|
||||||
|
literal=False,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
"""
|
"""
|
||||||
This is intended for ad-hoc "small" grids with static data. Renders
|
This is intended for ad-hoc "small" grids with static data. Renders
|
||||||
|
@ -1239,7 +1240,10 @@ class Grid(WuttaGrid):
|
||||||
if context['paginated']:
|
if context['paginated']:
|
||||||
context.setdefault('per_page', 20)
|
context.setdefault('per_page', 20)
|
||||||
context['view_click_handler'] = self.get_view_click_handler()
|
context['view_click_handler'] = self.get_view_click_handler()
|
||||||
return render(template, context)
|
result = render(template, context)
|
||||||
|
if literal:
|
||||||
|
result = HTML.literal(result)
|
||||||
|
return result
|
||||||
|
|
||||||
def get_view_click_handler(self):
|
def get_view_click_handler(self):
|
||||||
""" """
|
""" """
|
||||||
|
@ -1544,6 +1548,11 @@ 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,6 +394,11 @@ class TailboneMenuHandler(WuttaMenuHandler):
|
||||||
'route': 'products',
|
'route': 'products',
|
||||||
'perm': 'products.list',
|
'perm': 'products.list',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'title': "Product Costs",
|
||||||
|
'route': 'product_costs',
|
||||||
|
'perm': 'product_costs.list',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'title': "Departments",
|
'title': "Departments",
|
||||||
'route': 'departments',
|
'route': 'departments',
|
||||||
|
@ -451,6 +456,11 @@ class TailboneMenuHandler(WuttaMenuHandler):
|
||||||
'route': 'vendors',
|
'route': 'vendors',
|
||||||
'perm': 'vendors.list',
|
'perm': 'vendors.list',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'title': "Product Costs",
|
||||||
|
'route': 'product_costs',
|
||||||
|
'perm': 'product_costs.list',
|
||||||
|
},
|
||||||
{'type': 'sep'},
|
{'type': 'sep'},
|
||||||
{
|
{
|
||||||
'title': "Ordering",
|
'title': "Ordering",
|
||||||
|
|
|
@ -632,9 +632,23 @@
|
||||||
% endif
|
% endif
|
||||||
<div class="navbar-dropdown">
|
<div class="navbar-dropdown">
|
||||||
% if request.is_root:
|
% if request.is_root:
|
||||||
${h.link_to("Stop being root", url('stop_root'), class_='navbar-item root-user')}
|
${h.form(url('stop_root'), ref='stopBeingRootForm')}
|
||||||
|
${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.link_to("Become root", url('become_root'), class_='navbar-item root-user')}
|
${h.form(url('become_root'), ref='startBeingRootForm')}
|
||||||
|
${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')}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<div i18n:domain="deform" tal:omit-tag=""
|
<div i18n:domain="deform" tal:omit-tag=""
|
||||||
tal:define="oid oid|field.oid;
|
tal:define="oid oid|field.oid;
|
||||||
name name|field.name;
|
name name|field.name;
|
||||||
|
vmodel vmodel|'field_model_' + name;
|
||||||
css_class css_class|field.widget.css_class;
|
css_class css_class|field.widget.css_class;
|
||||||
style style|field.widget.style;">
|
style style|field.widget.style;">
|
||||||
|
|
||||||
|
@ -8,7 +9,7 @@
|
||||||
${field.start_mapping()}
|
${field.start_mapping()}
|
||||||
<b-input type="password"
|
<b-input type="password"
|
||||||
name="${name}"
|
name="${name}"
|
||||||
value="${field.widget.redisplay and cstruct or ''}"
|
v-model="${vmodel}"
|
||||||
tal:attributes="class string: form-control ${css_class or ''};
|
tal:attributes="class string: form-control ${css_class or ''};
|
||||||
style style;
|
style style;
|
||||||
attributes|field.widget.attributes|{};"
|
attributes|field.widget.attributes|{};"
|
||||||
|
@ -18,7 +19,6 @@
|
||||||
</b-input>
|
</b-input>
|
||||||
<b-input type="password"
|
<b-input type="password"
|
||||||
name="${name}-confirm"
|
name="${name}-confirm"
|
||||||
value="${field.widget.redisplay and confirm or ''}"
|
|
||||||
tal:attributes="class string: form-control ${css_class or ''};
|
tal:attributes="class string: form-control ${css_class or ''};
|
||||||
style style;
|
style style;
|
||||||
confirm_attributes|field.widget.confirm_attributes|{};"
|
confirm_attributes|field.widget.confirm_attributes|{};"
|
||||||
|
|
|
@ -196,6 +196,7 @@
|
||||||
|
|
||||||
<p class="block has-text-weight-bold">
|
<p class="block has-text-weight-bold">
|
||||||
{{ version.model_title }}
|
{{ version.model_title }}
|
||||||
|
({{ version.operation }})
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table class="diff monospace is-size-7"
|
<table class="diff monospace is-size-7"
|
||||||
|
|
74
tailbone/templates/ordering/configure.mako
Normal file
74
tailbone/templates/ordering/configure.mako
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
## -*- 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,19 +55,20 @@
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="render_form_template()">
|
<%def name="render_form_template()">
|
||||||
<script type="text/x-template" id="${form.component}-template">
|
<script type="text/x-template" id="${form.vue_tagname}-template">
|
||||||
${self.render_form_innards()}
|
${self.render_form_innards()}
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="modify_vue_vars()">
|
<%def name="modify_vue_vars()">
|
||||||
${parent.modify_vue_vars()}
|
${parent.modify_vue_vars()}
|
||||||
|
<% request.register_component(form.vue_tagname, form.vue_component) %>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
## TODO: ugh, an awful lot of duplicated code here (from /forms/deform.mako)
|
## TODO: ugh, an awful lot of duplicated code here (from /forms/deform.mako)
|
||||||
|
|
||||||
let ${form.vue_component} = {
|
let ${form.vue_component} = {
|
||||||
template: '#${form.component}-template',
|
template: '#${form.vue_tagname}-template',
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
## TODO: deprecate / remove the latter option here
|
## TODO: deprecate / remove the latter option here
|
||||||
|
|
|
@ -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 set, user must choose a "supported" vendor; otherwise they may choose "any" vendor.">
|
<b-field message="If not set, user must choose a "supported" vendor.">
|
||||||
<b-checkbox name="rattail.batch.purchase.supported_vendors_only"
|
<b-checkbox name="rattail.batch.purchase.allow_receiving_any_vendor"
|
||||||
v-model="simpleSettings['rattail.batch.purchase.supported_vendors_only']"
|
v-model="simpleSettings['rattail.batch.purchase.allow_receiving_any_vendor']"
|
||||||
native-value="true"
|
native-value="true"
|
||||||
@input="settingsNeedSaved = true">
|
@input="settingsNeedSaved = true">
|
||||||
Only allow batch for "supported" vendors
|
Allow receiving for <span class="has-text-weight-bold">any</span> vendor
|
||||||
</b-checkbox>
|
</b-checkbox>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
|
|
|
@ -45,11 +45,10 @@
|
||||||
<b-button @click="runReportShowDialog = false">
|
<b-button @click="runReportShowDialog = false">
|
||||||
Cancel
|
Cancel
|
||||||
</b-button>
|
</b-button>
|
||||||
${h.form(master.get_action_url('execute', instance))}
|
${h.form(master.get_action_url('execute', instance), **{'@submit': 'runReportSubmitting = true'})}
|
||||||
${h.csrf_token(request)}
|
${h.csrf_token(request)}
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
@click="runReportSubmitting = true"
|
|
||||||
:disabled="runReportSubmitting"
|
:disabled="runReportSubmitting"
|
||||||
icon-pack="fas"
|
icon-pack="fas"
|
||||||
icon-left="arrow-circle-right">
|
icon-left="arrow-circle-right">
|
||||||
|
|
|
@ -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="stopBeingRoot()"
|
<a @click="$refs.stopBeingRootForm.submit()"
|
||||||
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="startBeingRoot()"
|
<a @click="$refs.startBeingRootForm.submit()"
|
||||||
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,18 +1103,6 @@
|
||||||
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
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,28 +44,6 @@ class UserLogin(colander.MappingSchema):
|
||||||
widget=dfwidget.PasswordWidget())
|
widget=dfwidget.PasswordWidget())
|
||||||
|
|
||||||
|
|
||||||
@colander.deferred
|
|
||||||
def current_password_correct(node, kw):
|
|
||||||
request = kw['request']
|
|
||||||
app = request.rattail_config.get_app()
|
|
||||||
auth = app.get_auth_handler()
|
|
||||||
user = kw['user']
|
|
||||||
def validate(node, value):
|
|
||||||
if not auth.authenticate_user(Session(), user.username, value):
|
|
||||||
raise colander.Invalid(node, "The password is incorrect")
|
|
||||||
return validate
|
|
||||||
|
|
||||||
|
|
||||||
class ChangePassword(colander.MappingSchema):
|
|
||||||
|
|
||||||
current_password = colander.SchemaNode(colander.String(),
|
|
||||||
widget=dfwidget.PasswordWidget(),
|
|
||||||
validator=current_password_correct)
|
|
||||||
|
|
||||||
new_password = colander.SchemaNode(colander.String(),
|
|
||||||
widget=dfwidget.CheckedPasswordWidget())
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationView(View):
|
class AuthenticationView(View):
|
||||||
|
|
||||||
def forbidden(self):
|
def forbidden(self):
|
||||||
|
@ -116,10 +94,6 @@ class AuthenticationView(View):
|
||||||
else:
|
else:
|
||||||
self.request.session.flash("Invalid username or password", 'error')
|
self.request.session.flash("Invalid username or password", 'error')
|
||||||
|
|
||||||
image_url = self.rattail_config.get(
|
|
||||||
'tailbone', 'main_image_url',
|
|
||||||
default=self.request.static_url('tailbone:static/img/home_logo.png'))
|
|
||||||
|
|
||||||
# nb. hacky..but necessary, to add the refs, for autofocus
|
# nb. hacky..but necessary, to add the refs, for autofocus
|
||||||
# (also add key handler, so ENTER acts like TAB)
|
# (also add key handler, so ENTER acts like TAB)
|
||||||
dform = form.make_deform_form()
|
dform = form.make_deform_form()
|
||||||
|
@ -132,7 +106,6 @@ class AuthenticationView(View):
|
||||||
return {
|
return {
|
||||||
'form': form,
|
'form': form,
|
||||||
'referrer': referrer,
|
'referrer': referrer,
|
||||||
'image_url': image_url,
|
|
||||||
'index_title': app.get_node_title(),
|
'index_title': app.get_node_title(),
|
||||||
'help_url': global_help_url(self.rattail_config),
|
'help_url': global_help_url(self.rattail_config),
|
||||||
}
|
}
|
||||||
|
@ -181,7 +154,23 @@ class AuthenticationView(View):
|
||||||
self.request.user))
|
self.request.user))
|
||||||
return self.redirect(self.request.get_referrer())
|
return self.redirect(self.request.get_referrer())
|
||||||
|
|
||||||
schema = ChangePassword().bind(user=self.request.user, request=self.request)
|
def check_user_password(node, value):
|
||||||
|
auth = self.app.get_auth_handler()
|
||||||
|
user = self.request.user
|
||||||
|
if not auth.check_user_password(user, value):
|
||||||
|
node.raise_invalid("The password is incorrect")
|
||||||
|
|
||||||
|
schema = colander.Schema()
|
||||||
|
|
||||||
|
schema.add(colander.SchemaNode(colander.String(),
|
||||||
|
name='current_password',
|
||||||
|
widget=dfwidget.PasswordWidget(),
|
||||||
|
validator=check_user_password))
|
||||||
|
|
||||||
|
schema.add(colander.SchemaNode(colander.String(),
|
||||||
|
name='new_password',
|
||||||
|
widget=dfwidget.CheckedPasswordWidget()))
|
||||||
|
|
||||||
form = forms.Form(schema=schema, request=self.request)
|
form = forms.Form(schema=schema, request=self.request)
|
||||||
if form.validate():
|
if form.validate():
|
||||||
auth = self.app.get_auth_handler()
|
auth = self.app.get_auth_handler()
|
||||||
|
|
|
@ -46,10 +46,11 @@ 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__)
|
||||||
|
@ -441,7 +442,7 @@ class BatchMasterView(MasterView):
|
||||||
|
|
||||||
form = [
|
form = [
|
||||||
begin_form,
|
begin_form,
|
||||||
csrf_token(self.request),
|
render_csrf_token(self.request),
|
||||||
tags.hidden('complete', value=value),
|
tags.hidden('complete', value=value),
|
||||||
submit,
|
submit,
|
||||||
tags.end_form(),
|
tags.end_form(),
|
||||||
|
|
|
@ -412,7 +412,7 @@ class MasterView(View):
|
||||||
session = self.Session()
|
session = self.Session()
|
||||||
kwargs.setdefault('paginated', False)
|
kwargs.setdefault('paginated', False)
|
||||||
grid = self.make_grid(session=session, **kwargs)
|
grid = self.make_grid(session=session, **kwargs)
|
||||||
return grid.make_visible_data()
|
return grid.get_visible_data()
|
||||||
|
|
||||||
def get_grid_columns(self):
|
def get_grid_columns(self):
|
||||||
"""
|
"""
|
||||||
|
@ -903,7 +903,7 @@ class MasterView(View):
|
||||||
|
|
||||||
def valid_employee_uuid(self, node, value):
|
def valid_employee_uuid(self, node, value):
|
||||||
if value:
|
if value:
|
||||||
model = self.model
|
model = self.app.model
|
||||||
employee = self.Session.get(model.Employee, value)
|
employee = self.Session.get(model.Employee, value)
|
||||||
if not employee:
|
if not employee:
|
||||||
node.raise_invalid("Employee not found")
|
node.raise_invalid("Employee not found")
|
||||||
|
@ -939,7 +939,7 @@ class MasterView(View):
|
||||||
|
|
||||||
def valid_vendor_uuid(self, node, value):
|
def valid_vendor_uuid(self, node, value):
|
||||||
if value:
|
if value:
|
||||||
model = self.model
|
model = self.app.model
|
||||||
vendor = self.Session.get(model.Vendor, value)
|
vendor = self.Session.get(model.Vendor, value)
|
||||||
if not vendor:
|
if not vendor:
|
||||||
node.raise_invalid("Vendor not found")
|
node.raise_invalid("Vendor not found")
|
||||||
|
@ -1382,7 +1382,7 @@ class MasterView(View):
|
||||||
return classes
|
return classes
|
||||||
|
|
||||||
def make_revisions_grid(self, obj, empty_data=False):
|
def make_revisions_grid(self, obj, empty_data=False):
|
||||||
model = self.model
|
model = self.app.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
row_url = lambda txn, i: self.request.route_url(f'{route_prefix}.version',
|
row_url = lambda txn, i: self.request.route_url(f'{route_prefix}.version',
|
||||||
uuid=obj.uuid,
|
uuid=obj.uuid,
|
||||||
|
@ -1710,7 +1710,7 @@ class MasterView(View):
|
||||||
kwargs.setdefault('paginated', False)
|
kwargs.setdefault('paginated', False)
|
||||||
kwargs.setdefault('sortable', sort)
|
kwargs.setdefault('sortable', sort)
|
||||||
grid = self.make_row_grid(session=session, **kwargs)
|
grid = self.make_row_grid(session=session, **kwargs)
|
||||||
return grid.make_visible_data()
|
return grid.get_visible_data()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_row_url_prefix(cls):
|
def get_row_url_prefix(cls):
|
||||||
|
@ -2153,7 +2153,7 @@ class MasterView(View):
|
||||||
Thread target for executing an object.
|
Thread target for executing an object.
|
||||||
"""
|
"""
|
||||||
app = self.get_rattail_app()
|
app = self.get_rattail_app()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
session = app.make_session()
|
session = app.make_session()
|
||||||
obj = self.get_instance_for_key(key, session)
|
obj = self.get_instance_for_key(key, session)
|
||||||
user = session.get(model.User, user_uuid)
|
user = session.get(model.User, user_uuid)
|
||||||
|
@ -2594,7 +2594,7 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
# nb. self.Session may differ, so use tailbone.db.Session
|
# nb. self.Session may differ, so use tailbone.db.Session
|
||||||
session = Session()
|
session = Session()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
|
|
||||||
info = session.query(model.TailbonePageHelp)\
|
info = session.query(model.TailbonePageHelp)\
|
||||||
|
@ -2617,7 +2617,7 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
# nb. self.Session may differ, so use tailbone.db.Session
|
# nb. self.Session may differ, so use tailbone.db.Session
|
||||||
session = Session()
|
session = Session()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
|
|
||||||
info = session.query(model.TailbonePageHelp)\
|
info = session.query(model.TailbonePageHelp)\
|
||||||
|
@ -2639,7 +2639,7 @@ class MasterView(View):
|
||||||
|
|
||||||
# nb. self.Session may differ, so use tailbone.db.Session
|
# nb. self.Session may differ, so use tailbone.db.Session
|
||||||
session = Session()
|
session = Session()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
schema = colander.Schema()
|
schema = colander.Schema()
|
||||||
|
|
||||||
|
@ -2673,7 +2673,7 @@ class MasterView(View):
|
||||||
|
|
||||||
# nb. self.Session may differ, so use tailbone.db.Session
|
# nb. self.Session may differ, so use tailbone.db.Session
|
||||||
session = Session()
|
session = Session()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
route_prefix = self.get_route_prefix()
|
route_prefix = self.get_route_prefix()
|
||||||
schema = colander.Schema()
|
schema = colander.Schema()
|
||||||
|
|
||||||
|
@ -5541,7 +5541,7 @@ class MasterView(View):
|
||||||
input_file_templates=True,
|
input_file_templates=True,
|
||||||
output_file_templates=True):
|
output_file_templates=True):
|
||||||
app = self.get_rattail_app()
|
app = self.get_rattail_app()
|
||||||
model = self.model
|
model = self.app.model
|
||||||
names = []
|
names = []
|
||||||
|
|
||||||
if simple_settings is None:
|
if simple_settings is None:
|
||||||
|
@ -6100,7 +6100,7 @@ class MasterView(View):
|
||||||
renderer='json')
|
renderer='json')
|
||||||
|
|
||||||
|
|
||||||
class ViewSupplement(object):
|
class ViewSupplement:
|
||||||
"""
|
"""
|
||||||
Base class for view "supplements" - which are sort of like plugins
|
Base class for view "supplements" - which are sort of like plugins
|
||||||
which can "supplement" certain aspects of the view.
|
which can "supplement" certain aspects of the view.
|
||||||
|
@ -6127,6 +6127,7 @@ class ViewSupplement(object):
|
||||||
def __init__(self, master):
|
def __init__(self, master):
|
||||||
self.master = master
|
self.master = master
|
||||||
self.request = master.request
|
self.request = master.request
|
||||||
|
self.app = master.app
|
||||||
self.model = master.model
|
self.model = master.model
|
||||||
self.rattail_config = master.rattail_config
|
self.rattail_config = master.rattail_config
|
||||||
self.Session = master.Session
|
self.Session = master.Session
|
||||||
|
@ -6160,7 +6161,7 @@ class ViewSupplement(object):
|
||||||
This is accomplished by subjecting the current base query to a
|
This is accomplished by subjecting the current base query to a
|
||||||
join, e.g. something like::
|
join, e.g. something like::
|
||||||
|
|
||||||
model = self.model
|
model = self.app.model
|
||||||
query = query.outerjoin(model.MyExtension)
|
query = query.outerjoin(model.MyExtension)
|
||||||
return query
|
return query
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -564,15 +564,19 @@ class PersonView(MasterView):
|
||||||
Method which must return the base query for the profile's POS
|
Method which must return the base query for the profile's POS
|
||||||
Transactions grid data.
|
Transactions grid data.
|
||||||
"""
|
"""
|
||||||
app = self.get_rattail_app()
|
customer = self.app.get_customer(person)
|
||||||
customer = app.get_customer(person)
|
|
||||||
|
|
||||||
key_field = app.get_customer_key_field()
|
if customer:
|
||||||
customer_key = getattr(customer, key_field)
|
key_field = self.app.get_customer_key_field()
|
||||||
if customer_key is not None:
|
customer_key = getattr(customer, key_field)
|
||||||
customer_key = str(customer_key)
|
if customer_key is not None:
|
||||||
|
customer_key = str(customer_key)
|
||||||
|
else:
|
||||||
|
# nb. this should *not* match anything, so query returns
|
||||||
|
# no results..
|
||||||
|
customer_key = person.uuid
|
||||||
|
|
||||||
trainwreck = app.get_trainwreck_handler()
|
trainwreck = self.app.get_trainwreck_handler()
|
||||||
model = trainwreck.get_model()
|
model = trainwreck.get_model()
|
||||||
query = TrainwreckSession.query(model.Transaction)\
|
query = TrainwreckSession.query(model.Transaction)\
|
||||||
.filter(model.Transaction.customer_id == customer_key)
|
.filter(model.Transaction.customer_id == customer_key)
|
||||||
|
@ -1382,8 +1386,8 @@ class PersonView(MasterView):
|
||||||
}
|
}
|
||||||
|
|
||||||
if not context['users']:
|
if not context['users']:
|
||||||
context['suggested_username'] = auth.generate_unique_username(self.Session(),
|
context['suggested_username'] = auth.make_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, CustomerOrderItem
|
from rattail.db.model import Product, PendingProduct, ProductCost, CustomerOrderItem
|
||||||
from rattail.gpc import GPC
|
from rattail.gpc import GPC
|
||||||
from rattail.threads import Thread
|
from rattail.threads import Thread
|
||||||
from rattail.exceptions import LabelPrintingError
|
from rattail.exceptions import LabelPrintingError
|
||||||
|
@ -1857,7 +1857,8 @@ class ProductView(MasterView):
|
||||||
lookup_fields.append('alt_code')
|
lookup_fields.append('alt_code')
|
||||||
if lookup_fields:
|
if lookup_fields:
|
||||||
product = self.products_handler.locate_product_for_entry(
|
product = self.products_handler.locate_product_for_entry(
|
||||||
session, term, lookup_fields=lookup_fields)
|
session, term, lookup_fields=lookup_fields,
|
||||||
|
first_if_multiple=True)
|
||||||
if product:
|
if product:
|
||||||
final_results.append(self.search_normalize_result(product))
|
final_results.append(self.search_normalize_result(product))
|
||||||
|
|
||||||
|
@ -2668,6 +2669,78 @@ class PendingProductView(MasterView):
|
||||||
permission=f'{permission_prefix}.ignore_product')
|
permission=f'{permission_prefix}.ignore_product')
|
||||||
|
|
||||||
|
|
||||||
|
class ProductCostView(MasterView):
|
||||||
|
"""
|
||||||
|
Master view for Product Costs
|
||||||
|
"""
|
||||||
|
model_class = ProductCost
|
||||||
|
route_prefix = 'product_costs'
|
||||||
|
url_prefix = '/products/costs'
|
||||||
|
has_versions = True
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'_product_key_',
|
||||||
|
'vendor',
|
||||||
|
'preference',
|
||||||
|
'code',
|
||||||
|
'case_size',
|
||||||
|
'case_cost',
|
||||||
|
'pack_size',
|
||||||
|
'pack_cost',
|
||||||
|
'unit_cost',
|
||||||
|
]
|
||||||
|
|
||||||
|
def query(self, session):
|
||||||
|
""" """
|
||||||
|
query = super().query(session)
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# always join on Product
|
||||||
|
return query.join(model.Product)
|
||||||
|
|
||||||
|
def configure_grid(self, g):
|
||||||
|
""" """
|
||||||
|
super().configure_grid(g)
|
||||||
|
model = self.app.model
|
||||||
|
|
||||||
|
# product key
|
||||||
|
field = self.get_product_key_field()
|
||||||
|
g.set_renderer(field, self.render_product_key)
|
||||||
|
g.set_sorter(field, getattr(model.Product, field))
|
||||||
|
g.set_sort_defaults(field)
|
||||||
|
g.set_filter(field, getattr(model.Product, field))
|
||||||
|
|
||||||
|
# vendor
|
||||||
|
g.set_joiner('vendor', lambda q: q.join(model.Vendor))
|
||||||
|
g.set_sorter('vendor', model.Vendor.name)
|
||||||
|
g.set_filter('vendor', model.Vendor.name, label="Vendor Name")
|
||||||
|
|
||||||
|
def render_product_key(self, cost, field):
|
||||||
|
""" """
|
||||||
|
handler = self.app.get_products_handler()
|
||||||
|
return handler.render_product_key(cost.product)
|
||||||
|
|
||||||
|
def configure_form(self, f):
|
||||||
|
""" """
|
||||||
|
super().configure_form(f)
|
||||||
|
|
||||||
|
# product
|
||||||
|
f.set_renderer('product', self.render_product)
|
||||||
|
if 'product_uuid' in f and 'product' in f:
|
||||||
|
f.remove('product')
|
||||||
|
f.replace('product_uuid', 'product')
|
||||||
|
|
||||||
|
# vendor
|
||||||
|
f.set_renderer('vendor', self.render_vendor)
|
||||||
|
if 'vendor_uuid' in f and 'vendor' in f:
|
||||||
|
f.remove('vendor')
|
||||||
|
f.replace('vendor_uuid', 'vendor')
|
||||||
|
|
||||||
|
# futures
|
||||||
|
# TODO: should eventually show a subgrid here?
|
||||||
|
f.remove('futures')
|
||||||
|
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
||||||
|
@ -2677,6 +2750,9 @@ def defaults(config, **kwargs):
|
||||||
PendingProductView = kwargs.get('PendingProductView', base['PendingProductView'])
|
PendingProductView = kwargs.get('PendingProductView', base['PendingProductView'])
|
||||||
PendingProductView.defaults(config)
|
PendingProductView.defaults(config)
|
||||||
|
|
||||||
|
ProductCostView = kwargs.get('ProductCostView', base['ProductCostView'])
|
||||||
|
ProductCostView.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
|
@ -24,6 +24,8 @@
|
||||||
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
|
||||||
|
@ -67,6 +69,8 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
'store',
|
'store',
|
||||||
'buyer',
|
'buyer',
|
||||||
'vendor',
|
'vendor',
|
||||||
|
'description',
|
||||||
|
'workflow',
|
||||||
'department',
|
'department',
|
||||||
'purchase',
|
'purchase',
|
||||||
'vendor_email',
|
'vendor_email',
|
||||||
|
@ -158,6 +162,174 @@ 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)\
|
||||||
|
@ -226,20 +398,40 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
|
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
model = self.model
|
model = self.app.model
|
||||||
|
enum = self.app.enum
|
||||||
|
route_prefix = self.get_route_prefix()
|
||||||
|
|
||||||
|
today = self.app.today()
|
||||||
batch = f.model_instance
|
batch = f.model_instance
|
||||||
app = self.get_rattail_app()
|
workflow = self.request.matchdict.get('workflow_key')
|
||||||
today = app.localtime().date()
|
vendor_handler = self.app.get_vendor_handler()
|
||||||
|
|
||||||
# mode
|
# mode
|
||||||
f.set_enum('mode', self.enum.PURCHASE_BATCH_MODE)
|
f.set_enum('mode', 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.rattail_config.single_store()
|
single_store = self.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.rattail_config.get_store(self.Session())
|
store = self.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')
|
||||||
|
@ -263,7 +455,6 @@ 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)\
|
||||||
|
@ -313,7 +504,7 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
if buyer:
|
if buyer:
|
||||||
buyer_display = str(buyer)
|
buyer_display = str(buyer)
|
||||||
elif self.creating:
|
elif self.creating:
|
||||||
buyer = app.get_employee(self.request.user)
|
buyer = self.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)
|
||||||
|
@ -324,6 +515,30 @@ 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)
|
||||||
|
@ -341,7 +556,7 @@ class PurchasingBatchView(BatchMasterView):
|
||||||
if vendor:
|
if vendor:
|
||||||
kwargs['vendor'] = vendor
|
kwargs['vendor'] = vendor
|
||||||
|
|
||||||
parsers = self.handler.get_supported_invoice_parsers(**kwargs)
|
parsers = self.batch_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)
|
||||||
|
@ -400,6 +615,35 @@ 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:
|
||||||
|
@ -515,10 +759,12 @@ 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.model
|
model = self.app.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:
|
||||||
|
@ -536,6 +782,11 @@ 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:
|
||||||
|
@ -919,6 +1170,25 @@ 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-2023 Lance Edgar
|
# Copyright © 2010-2024 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -28,14 +28,10 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,6 +47,8 @@ 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",
|
||||||
|
@ -59,9 +57,14 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
form_fields = [
|
form_fields = [
|
||||||
'id',
|
'id',
|
||||||
'store',
|
'store',
|
||||||
'buyer',
|
|
||||||
'vendor',
|
'vendor',
|
||||||
|
'description',
|
||||||
|
'workflow',
|
||||||
|
'order_file',
|
||||||
|
'order_parser_key',
|
||||||
|
'buyer',
|
||||||
'department',
|
'department',
|
||||||
|
'params',
|
||||||
'purchase',
|
'purchase',
|
||||||
'vendor_email',
|
'vendor_email',
|
||||||
'vendor_fax',
|
'vendor_fax',
|
||||||
|
@ -132,15 +135,26 @@ 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(OrderingBatchView, self).configure_form(f)
|
super().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(OrderingBatchView, self).get_batch_kwargs(batch, **kwargs)
|
kwargs = super().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
|
||||||
|
@ -155,7 +169,7 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
* ``cases_ordered``
|
* ``cases_ordered``
|
||||||
* ``units_ordered``
|
* ``units_ordered``
|
||||||
"""
|
"""
|
||||||
super(OrderingBatchView, self).configure_row_form(f)
|
super().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:
|
||||||
|
@ -308,7 +322,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 = localtime(self.rattail_config).date()
|
order_date = self.app.today()
|
||||||
|
|
||||||
return self.render_to_response('worksheet', {
|
return self.render_to_response('worksheet', {
|
||||||
'batch': batch,
|
'batch': batch,
|
||||||
|
@ -369,6 +383,7 @@ 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:
|
||||||
|
@ -478,13 +493,75 @@ 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(OrderingBatchView, self).get_execute_success_url(batch, result, **kwargs)
|
return super().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',
|
||||||
'receiving_workflow',
|
'workflow',
|
||||||
'truck_dump',
|
'truck_dump',
|
||||||
'truck_dump_children_first',
|
'truck_dump_children_first',
|
||||||
'truck_dump_children',
|
'truck_dump_children',
|
||||||
|
@ -235,135 +235,18 @@ 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 create(self, form=None, **kwargs):
|
def get_supported_vendors(self):
|
||||||
"""
|
""" """
|
||||||
Custom view for creating a new receiving batch. We split the process
|
vendor_handler = self.app.get_vendor_handler()
|
||||||
into two steps, 1) choose and 2) create. This is because the specific
|
vendors = {}
|
||||||
form details for creating a batch will depend on which "type" of batch
|
for parser in self.batch_handler.get_supported_invoice_parsers():
|
||||||
creation is to be done, and it's much easier to keep conditional logic
|
if parser.vendor_key:
|
||||||
for that in the server instead of client-side etc.
|
vendor = vendor_handler.get_vendor(self.Session(),
|
||||||
|
parser.vendor_key)
|
||||||
See also
|
if vendor:
|
||||||
:meth:`tailbone.views.purchasing.costing:CostingBatchView.create()`
|
vendors[vendor.uuid] = vendor
|
||||||
which uses similar logic.
|
vendors = sorted(vendors.values(), key=lambda v: v.name)
|
||||||
"""
|
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):
|
||||||
|
|
||||||
|
@ -404,13 +287,7 @@ 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))
|
||||||
|
|
||||||
# receiving_workflow
|
# TODO: remove this
|
||||||
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())
|
||||||
|
@ -525,7 +402,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('receiving_workflow') == 'from_multi_invoice'):
|
and batch.get_param('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')
|
||||||
|
@ -624,12 +501,6 @@ 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)
|
||||||
|
|
||||||
|
@ -654,42 +525,40 @@ 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']
|
||||||
|
|
||||||
# TODO: ugh should just have workflow and no batch_type
|
workflow = kwargs['workflow']
|
||||||
kwargs['receiving_workflow'] = batch_type
|
if workflow == 'from_scratch':
|
||||||
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 batch_type == 'from_invoice':
|
elif workflow == 'from_invoice':
|
||||||
pass
|
pass
|
||||||
elif batch_type == 'from_multi_invoice':
|
elif workflow == 'from_multi_invoice':
|
||||||
pass
|
pass
|
||||||
elif batch_type == 'from_po':
|
elif workflow == '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 batch_type == 'from_po_with_invoice':
|
elif workflow == '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 batch_type == 'truck_dump_children_first':
|
elif workflow == '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 batch_type == 'truck_dump_children_last':
|
elif workflow == '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 batch_type.startswith('truck_dump_child'):
|
elif workflow.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
|
||||||
|
@ -1986,6 +1855,12 @@ 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},
|
||||||
|
@ -2036,6 +1911,7 @@ 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)
|
||||||
|
|
||||||
|
@ -2043,17 +1919,11 @@ 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),
|
||||||
|
@ -2106,33 +1976,6 @@ 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(),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue