diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8ed82c5d..3c31ae92 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,16 +5,6 @@ All notable changes to Tailbone will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
-## 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
diff --git a/pyproject.toml b/pyproject.toml
index b928ec9b..5b63a71f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
[project]
name = "Tailbone"
-version = "0.22.0"
+version = "0.21.11"
description = "Backoffice Web Application for Rattail"
readme = "README.md"
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
diff --git a/tailbone/api/batch/receiving.py b/tailbone/api/batch/receiving.py
index b23bff55..daa4290f 100644
--- a/tailbone/api/batch/receiving.py
+++ b/tailbone/api/batch/receiving.py
@@ -29,7 +29,8 @@ import logging
import humanize
import sqlalchemy as sa
-from rattail.db.model import PurchaseBatch, PurchaseBatchRow
+from rattail.db import model
+from rattail.util import pretty_quantity
from cornice import Service
from deform import widget as dfwidget
@@ -44,7 +45,7 @@ log = logging.getLogger(__name__)
class ReceivingBatchViews(APIBatchView):
- model_class = PurchaseBatch
+ model_class = model.PurchaseBatch
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
route_prefix = 'receivingbatchviews'
permission_prefix = 'receiving'
@@ -54,8 +55,7 @@ class ReceivingBatchViews(APIBatchView):
supports_execute = True
def base_query(self):
- model = self.app.model
- query = super().base_query()
+ query = super(ReceivingBatchViews, self).base_query()
query = query.filter(model.PurchaseBatch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING)
return query
@@ -85,7 +85,7 @@ class ReceivingBatchViews(APIBatchView):
# assume "receive from PO" if given a PO key
if data.get('purchase_key'):
- data['workflow'] = 'from_po'
+ data['receiving_workflow'] = 'from_po'
return super().create_object(data)
@@ -120,7 +120,6 @@ class ReceivingBatchViews(APIBatchView):
return self._get(obj=batch)
def eligible_purchases(self):
- model = self.app.model
uuid = self.request.params.get('vendor_uuid')
vendor = self.Session.get(model.Vendor, uuid) if uuid else None
if not vendor:
@@ -177,7 +176,7 @@ class ReceivingBatchViews(APIBatchView):
class ReceivingBatchRowViews(APIBatchRowView):
- model_class = PurchaseBatchRow
+ model_class = model.PurchaseBatchRow
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
route_prefix = 'receiving.rows'
permission_prefix = 'receiving'
@@ -186,8 +185,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
supports_quick_entry = True
def make_filter_spec(self):
- model = self.app.model
- filters = super().make_filter_spec()
+ filters = super(ReceivingBatchRowViews, self).make_filter_spec()
if filters:
# must translate certain convenience filters
@@ -298,11 +296,11 @@ class ReceivingBatchRowViews(APIBatchRowView):
return filters
def normalize(self, row):
- data = super().normalize(row)
- model = self.app.model
+ data = super(ReceivingBatchRowViews, self).normalize(row)
batch = row.batch
- prodder = self.app.get_products_handler()
+ app = self.get_rattail_app()
+ prodder = app.get_products_handler()
data['product_uuid'] = row.product_uuid
data['item_id'] = row.item_id
@@ -377,7 +375,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
if accounted_for:
# some product accounted for; button should receive "remainder" only
if remainder:
- remainder = self.app.render_quantity(remainder)
+ remainder = pretty_quantity(remainder)
data['quick_receive_quantity'] = remainder
data['quick_receive_text'] = "Receive Remainder ({} {})".format(
remainder, data['unit_uom'])
@@ -388,7 +386,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
else: # nothing yet accounted for, button should receive "all"
if not remainder:
log.warning("quick receive remainder is empty for row %s", row.uuid)
- remainder = self.app.render_quantity(remainder)
+ remainder = pretty_quantity(remainder)
data['quick_receive_quantity'] = remainder
data['quick_receive_text'] = "Receive ALL ({} {})".format(
remainder, data['unit_uom'])
@@ -416,7 +414,7 @@ class ReceivingBatchRowViews(APIBatchRowView):
data['received_alert'] = None
if self.batch_handler.get_units_confirmed(row):
msg = "You have already received some of this product; last update was {}.".format(
- humanize.naturaltime(self.app.make_utc() - row.modified))
+ humanize.naturaltime(app.make_utc() - row.modified))
data['received_alert'] = msg
return data
@@ -425,8 +423,6 @@ class ReceivingBatchRowViews(APIBatchRowView):
"""
View which handles "receiving" against a particular batch row.
"""
- model = self.app.model
-
# first do basic input validation
schema = ReceiveRow().bind(session=self.Session())
form = forms.Form(schema=schema, request=self.request)
diff --git a/tailbone/templates/ordering/configure.mako b/tailbone/templates/ordering/configure.mako
deleted file mode 100644
index dc505c42..00000000
--- a/tailbone/templates/ordering/configure.mako
+++ /dev/null
@@ -1,74 +0,0 @@
-## -*- coding: utf-8; -*-
-<%inherit file="/configure.mako" />
-
-<%def name="form_content()">
-
-
Workflows
-
-
-
- Users can only choose from the workflows enabled below.
-
-
-
-
- From Scratch
-
-
-
-
-
- From Order File
-
-
-
-
-
- Vendors
-
-
-
-
- Allow ordering for any vendor
-
-
-
-
-
- Order Parsers
-
-
-
- Only the selected file parsers will be exposed to users.
-
-
- % for Parser in order_parsers:
-
-
- ${Parser.title}
-
-
- % endfor
-
-
-
-%def>
-
-<%def name="modify_vue_vars()">
- ${parent.modify_vue_vars()}
-
-%def>
diff --git a/tailbone/templates/receiving/configure.mako b/tailbone/templates/receiving/configure.mako
index a36dde43..f613e13e 100644
--- a/tailbone/templates/receiving/configure.mako
+++ b/tailbone/templates/receiving/configure.mako
@@ -69,12 +69,12 @@
Vendors
-
-
+
- Allow receiving for any vendor
+ Only allow batch for "supported" vendors
diff --git a/tailbone/views/batch/core.py b/tailbone/views/batch/core.py
index c162b579..a75fda1c 100644
--- a/tailbone/views/batch/core.py
+++ b/tailbone/views/batch/core.py
@@ -46,11 +46,10 @@ import colander
from deform import widget as dfwidget
from webhelpers2.html import HTML, tags
-from wuttaweb.util import render_csrf_token
-
from tailbone import forms, grids
from tailbone.db import Session
from tailbone.views import MasterView
+from tailbone.util import csrf_token
log = logging.getLogger(__name__)
@@ -442,7 +441,7 @@ class BatchMasterView(MasterView):
form = [
begin_form,
- render_csrf_token(self.request),
+ csrf_token(self.request),
tags.hidden('complete', value=value),
submit,
tags.end_form(),
diff --git a/tailbone/views/people.py b/tailbone/views/people.py
index d288b551..b6a4c0b9 100644
--- a/tailbone/views/people.py
+++ b/tailbone/views/people.py
@@ -1382,8 +1382,8 @@ class PersonView(MasterView):
}
if not context['users']:
- context['suggested_username'] = auth.make_unique_username(self.Session(),
- person=person)
+ context['suggested_username'] = auth.generate_unique_username(self.Session(),
+ person=person)
return context
diff --git a/tailbone/views/purchasing/batch.py b/tailbone/views/purchasing/batch.py
index 5e00704e..590b9af5 100644
--- a/tailbone/views/purchasing/batch.py
+++ b/tailbone/views/purchasing/batch.py
@@ -24,8 +24,6 @@
Base class for purchasing batch views
"""
-import warnings
-
from rattail.db.model import PurchaseBatch, PurchaseBatchRow
import colander
@@ -69,8 +67,6 @@ class PurchasingBatchView(BatchMasterView):
'store',
'buyer',
'vendor',
- 'description',
- 'workflow',
'department',
'purchase',
'vendor_email',
@@ -162,174 +158,6 @@ class PurchasingBatchView(BatchMasterView):
def batch_mode(self):
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):
model = self.model
return session.query(model.PurchaseBatch)\
@@ -398,40 +226,20 @@ class PurchasingBatchView(BatchMasterView):
def configure_form(self, f):
super().configure_form(f)
- model = self.app.model
- enum = self.app.enum
- route_prefix = self.get_route_prefix()
-
- today = self.app.today()
+ model = self.model
batch = f.model_instance
- workflow = self.request.matchdict.get('workflow_key')
- vendor_handler = self.app.get_vendor_handler()
+ app = self.get_rattail_app()
+ today = app.localtime().date()
# 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')
+ f.set_enum('mode', self.enum.PURCHASE_BATCH_MODE)
# store
- single_store = self.config.single_store()
+ single_store = self.rattail_config.single_store()
if self.creating:
f.replace('store', 'store_uuid')
if single_store:
- store = self.config.get_store(self.Session())
+ store = self.rattail_config.get_store(self.Session())
f.set_widget('store_uuid', dfwidget.HiddenWidget())
f.set_default('store_uuid', store.uuid)
f.set_hidden('store_uuid')
@@ -455,6 +263,7 @@ class PurchasingBatchView(BatchMasterView):
if self.creating:
f.replace('vendor', 'vendor_uuid')
f.set_label('vendor_uuid', "Vendor")
+ vendor_handler = app.get_vendor_handler()
use_dropdown = vendor_handler.choice_uses_dropdown()
if use_dropdown:
vendors = self.Session.query(model.Vendor)\
@@ -504,7 +313,7 @@ class PurchasingBatchView(BatchMasterView):
if buyer:
buyer_display = str(buyer)
elif self.creating:
- buyer = self.app.get_employee(self.request.user)
+ buyer = app.get_employee(self.request.user)
if buyer:
buyer_display = str(buyer)
f.set_default('buyer_uuid', buyer.uuid)
@@ -515,30 +324,6 @@ class PurchasingBatchView(BatchMasterView):
field_display=buyer_display, service_url=buyers_url))
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
if self.creating:
f.set_type('invoice_file', 'file', required=False)
@@ -556,7 +341,7 @@ class PurchasingBatchView(BatchMasterView):
if vendor:
kwargs['vendor'] = vendor
- parsers = self.batch_handler.get_supported_invoice_parsers(**kwargs)
+ parsers = self.handler.get_supported_invoice_parsers(**kwargs)
parser_values = [(p.key, p.display) for p in parsers]
if len(parsers) == 1:
f.set_default('invoice_parser_key', parsers[0].key)
@@ -615,35 +400,6 @@ class PurchasingBatchView(BatchMasterView):
'vendor_contact',
'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):
store = batch.store
if not store:
@@ -759,12 +515,10 @@ class PurchasingBatchView(BatchMasterView):
def get_batch_kwargs(self, batch, **kwargs):
kwargs = super().get_batch_kwargs(batch, **kwargs)
- model = self.app.model
+ model = self.model
kwargs['mode'] = self.batch_mode
- kwargs['workflow'] = self.request.POST['workflow']
kwargs['truck_dump'] = batch.truck_dump
- kwargs['order_parser_key'] = batch.order_parser_key
kwargs['invoice_parser_key'] = batch.invoice_parser_key
if batch.store:
@@ -782,11 +536,6 @@ class PurchasingBatchView(BatchMasterView):
elif 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:
kwargs['department'] = batch.department
elif batch.department_uuid:
@@ -1170,25 +919,6 @@ class PurchasingBatchView(BatchMasterView):
# # otherwise just view batch again
# 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):
diff --git a/tailbone/views/purchasing/ordering.py b/tailbone/views/purchasing/ordering.py
index c7cc7bfc..2e24eebb 100644
--- a/tailbone/views/purchasing/ordering.py
+++ b/tailbone/views/purchasing/ordering.py
@@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
-# Copyright © 2010-2024 Lance Edgar
+# Copyright © 2010-2023 Lance Edgar
#
# This file is part of Rattail.
#
@@ -28,10 +28,14 @@ import os
import json
import openpyxl
+from sqlalchemy import orm
+from rattail.db import model, api
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
@@ -47,8 +51,6 @@ class OrderingBatchView(PurchasingBatchView):
rows_editable = True
has_worksheet = True
default_help_url = 'https://rattailproject.org/docs/rattail-manual/features/purchasing/ordering/index.html'
- downloadable = True
- configurable = True
labels = {
'po_total_calculated': "PO Total",
@@ -57,14 +59,9 @@ class OrderingBatchView(PurchasingBatchView):
form_fields = [
'id',
'store',
- 'vendor',
- 'description',
- 'workflow',
- 'order_file',
- 'order_parser_key',
'buyer',
+ 'vendor',
'department',
- 'params',
'purchase',
'vendor_email',
'vendor_fax',
@@ -135,26 +132,15 @@ class OrderingBatchView(PurchasingBatchView):
return self.enum.PURCHASE_BATCH_MODE_ORDERING
def configure_form(self, f):
- super().configure_form(f)
+ super(OrderingBatchView, self).configure_form(f)
batch = f.model_instance
- workflow = self.request.matchdict.get('workflow_key')
# purchase
if self.creating or not batch.executed or not batch.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):
- kwargs = super().get_batch_kwargs(batch, **kwargs)
+ kwargs = super(OrderingBatchView, self).get_batch_kwargs(batch, **kwargs)
kwargs['ship_method'] = batch.ship_method
kwargs['notes_to_vendor'] = batch.notes_to_vendor
return kwargs
@@ -169,7 +155,7 @@ class OrderingBatchView(PurchasingBatchView):
* ``cases_ordered``
* ``units_ordered``
"""
- super().configure_row_form(f)
+ super(OrderingBatchView, self).configure_row_form(f)
# when editing, only certain fields should allow changes
if self.editing:
@@ -322,7 +308,7 @@ class OrderingBatchView(PurchasingBatchView):
title = self.get_instance_title(batch)
order_date = batch.date_ordered
if not order_date:
- order_date = self.app.today()
+ order_date = localtime(self.rattail_config).date()
return self.render_to_response('worksheet', {
'batch': batch,
@@ -383,7 +369,6 @@ class OrderingBatchView(PurchasingBatchView):
of being updated. If a matching row is not found, it will not be
created.
"""
- model = self.app.model
batch = self.get_instance()
try:
@@ -493,75 +478,13 @@ class OrderingBatchView(PurchasingBatchView):
return self.file_response(path)
def get_execute_success_url(self, batch, result, **kwargs):
- model = self.app.model
if isinstance(result, model.Purchase):
return self.request.route_url('purchases.view', uuid=result.uuid)
- 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)
+ return super(OrderingBatchView, self).get_execute_success_url(batch, result, **kwargs)
@classmethod
def defaults(cls, config):
cls._ordering_defaults(config)
- cls._purchase_batch_defaults(config)
cls._batch_defaults(config)
cls._defaults(config)
diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py
index 01858c98..de19a2b9 100644
--- a/tailbone/views/purchasing/receiving.py
+++ b/tailbone/views/purchasing/receiving.py
@@ -108,7 +108,7 @@ class ReceivingBatchView(PurchasingBatchView):
'store',
'vendor',
'description',
- 'workflow',
+ 'receiving_workflow',
'truck_dump',
'truck_dump_children_first',
'truck_dump_children',
@@ -235,18 +235,135 @@ class ReceivingBatchView(PurchasingBatchView):
if not self.handler.allow_truck_dump_receiving():
g.remove('truck_dump')
- def get_supported_vendors(self):
- """ """
- vendor_handler = self.app.get_vendor_handler()
- 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)
- return vendors
+ def create(self, form=None, **kwargs):
+ """
+ Custom view for creating a new receiving batch. We split the process
+ into two steps, 1) choose and 2) create. 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.
+
+ See also
+ :meth:`tailbone.views.purchasing.costing:CostingBatchView.create()`
+ which uses similar logic.
+ """
+ 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):
@@ -287,7 +404,13 @@ class ReceivingBatchView(PurchasingBatchView):
# cancel should take us back to choosing a workflow
f.cancel_url = self.request.route_url('{}.create'.format(route_prefix))
- # TODO: remove this
+ # receiving_workflow
+ if self.creating and workflow:
+ f.set_readonly('receiving_workflow')
+ f.set_renderer('receiving_workflow', self.render_receiving_workflow)
+ else:
+ f.remove('receiving_workflow')
+
# batch_type
if self.creating:
f.set_widget('batch_type', dfwidget.HiddenWidget())
@@ -402,7 +525,7 @@ class ReceivingBatchView(PurchasingBatchView):
# multiple invoice files (if applicable)
if (not self.creating
- and batch.get_param('workflow') == 'from_multi_invoice'):
+ and batch.get_param('receiving_workflow') == 'from_multi_invoice'):
if 'invoice_files' not in f:
f.insert_before('invoice_file', 'invoice_files')
@@ -501,6 +624,12 @@ class ReceivingBatchView(PurchasingBatchView):
items.append(HTML.tag('li', c=[link]))
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):
params = super().get_visible_params(batch)
@@ -525,40 +654,42 @@ class ReceivingBatchView(PurchasingBatchView):
def get_batch_kwargs(self, 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
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']
- workflow = kwargs['workflow']
- if workflow == 'from_scratch':
+ # TODO: ugh should just have workflow and no batch_type
+ kwargs['receiving_workflow'] = batch_type
+ if batch_type == 'from_scratch':
kwargs.pop('truck_dump_batch', None)
kwargs.pop('truck_dump_batch_uuid', None)
- elif workflow == 'from_invoice':
+ elif batch_type == 'from_invoice':
pass
- elif workflow == 'from_multi_invoice':
+ elif batch_type == 'from_multi_invoice':
pass
- elif workflow == 'from_po':
+ elif batch_type == 'from_po':
# TODO: how to best handle this field? this doesn't seem flexible
kwargs['purchase_key'] = batch.purchase_uuid
- elif workflow == 'from_po_with_invoice':
+ elif batch_type == 'from_po_with_invoice':
# TODO: how to best handle this field? this doesn't seem flexible
kwargs['purchase_key'] = batch.purchase_uuid
- elif workflow == 'truck_dump_children_first':
+ elif batch_type == 'truck_dump_children_first':
kwargs['truck_dump'] = True
kwargs['truck_dump_children_first'] = True
kwargs['order_quantities_known'] = True
# TODO: this makes sense in some cases, but all?
# (should just omit that field when not relevant)
kwargs['date_ordered'] = None
- elif workflow == 'truck_dump_children_last':
+ elif batch_type == 'truck_dump_children_last':
kwargs['truck_dump'] = True
kwargs['truck_dump_ready'] = True
# TODO: this makes sense in some cases, but all?
# (should just omit that field when not relevant)
kwargs['date_ordered'] = None
- elif workflow.startswith('truck_dump_child'):
+ elif batch_type.startswith('truck_dump_child'):
truck_dump = self.get_instance()
kwargs['store'] = truck_dump.store
kwargs['vendor'] = truck_dump.vendor
@@ -1855,12 +1986,6 @@ class ReceivingBatchView(PurchasingBatchView):
'type': bool},
# 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',
'option': 'purchase.supported_vendors_only',
'type': bool},
@@ -1911,7 +2036,6 @@ class ReceivingBatchView(PurchasingBatchView):
@classmethod
def defaults(cls, config):
cls._receiving_defaults(config)
- cls._purchase_batch_defaults(config)
cls._batch_defaults(config)
cls._defaults(config)
@@ -1919,11 +2043,17 @@ class ReceivingBatchView(PurchasingBatchView):
def _receiving_defaults(cls, config):
rattail_config = config.registry.settings.get('rattail_config')
route_prefix = cls.get_route_prefix()
+ url_prefix = cls.get_url_prefix()
instance_url_prefix = cls.get_instance_url_prefix()
model_key = cls.get_model_key()
model_title = cls.get_model_title()
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
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),
@@ -1976,6 +2106,33 @@ class ReceivingBatchView(PurchasingBatchView):
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):
mode = colander.SchemaNode(colander.String(),