From a4f2b6d5c20df215ef7412ecf52dca40a4c9ff40 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 6 Aug 2012 15:05:09 -0700 Subject: [PATCH] update grids per edbob changes, add order worksheet report --- rattail/pyramid/forms.py | 12 +- .../pyramid/reports/ordering_worksheet.mako | 115 +++++++++++++++ rattail/pyramid/templates/products/index.mako | 2 +- rattail/pyramid/templates/reports/base.mako | 2 + .../pyramid/templates/reports/ordering.mako | 84 +++++++++++ rattail/pyramid/views/departments.py | 134 ++++++++++-------- rattail/pyramid/views/employees.py | 56 ++++---- rattail/pyramid/views/products.py | 62 ++++---- rattail/pyramid/views/reports.py | 94 ++++++++++++ rattail/pyramid/views/vendors.py | 41 +++--- 10 files changed, 446 insertions(+), 156 deletions(-) create mode 100644 rattail/pyramid/reports/ordering_worksheet.mako create mode 100644 rattail/pyramid/templates/reports/base.mako create mode 100644 rattail/pyramid/templates/reports/ordering.mako create mode 100644 rattail/pyramid/views/reports.py diff --git a/rattail/pyramid/forms.py b/rattail/pyramid/forms.py index 12ec37e2..c768327b 100644 --- a/rattail/pyramid/forms.py +++ b/rattail/pyramid/forms.py @@ -26,6 +26,8 @@ ``rattail.pyramid.forms`` -- Rattail Forms """ +from webhelpers.html import literal + import formalchemy # from formalchemy.fields import SelectFieldRenderer @@ -70,11 +72,11 @@ class PriceFieldRenderer(formalchemy.FieldRenderer): if price: if price.price is not None and price.pack_price is not None: if price.multiple > 1: - return '$ %0.2f / %u  ($ %0.2f / %u)' % ( - price.price, price.multiple, - price.pack_price, price.pack_multiple) - return '$ %0.2f  ($ %0.2f / %u)' % ( - price.price, price.pack_price, price.pack_multiple) + return literal('$ %0.2f / %u  ($ %0.2f / %u)' % ( + price.price, price.multiple, + price.pack_price, price.pack_multiple)) + return literal('$ %0.2f  ($ %0.2f / %u)' % ( + price.price, price.pack_price, price.pack_multiple)) if price.price is not None: if price.multiple > 1: return '$ %0.2f / %u' % (price.price, price.multiple) diff --git a/rattail/pyramid/reports/ordering_worksheet.mako b/rattail/pyramid/reports/ordering_worksheet.mako new file mode 100644 index 00000000..d54cf820 --- /dev/null +++ b/rattail/pyramid/reports/ordering_worksheet.mako @@ -0,0 +1,115 @@ + + + + + Ordering Worksheet : ${vendor.name} + + + + +

Ordering Worksheet

+

Vendor:  ${vendor.name} (${vendor.id})

+

Phone:  ${vendor.phone or ''}

+

Contact:  ${vendor.contact or ''}

+

generated on ${date} at ${time}

+
+ + + % for dept in sorted(costs, key=lambda x: x.name): + + + + % for subdept in sorted(costs[dept], key=lambda x: x.name): + + + + + + + + + + + + % for cost in sorted(costs[dept][subdept], key=lambda x: x.product.description): + + + + + + + % for i in range(14): + + % endfor + + % endfor + + + % endfor + % endfor +
Department:  ${dept.name} (${dept.number})
Subdepartment:  ${subdept.name} (${subdept.number})
UPCDescriptionCase Qty.Vend. CodePreferredOrder Scratch Pad
${get_upc(cost.product)}${cost.product.description}${cost.case_size} ${rattail.UNIT_OF_MEASURE.get(cost.product.unit_of_measure, '')}${cost.code or ''}${'X' if cost.preference == 1 else ''} 
+
+ + diff --git a/rattail/pyramid/templates/products/index.mako b/rattail/pyramid/templates/products/index.mako index faba2e9e..b6820c96 100644 --- a/rattail/pyramid/templates/products/index.mako +++ b/rattail/pyramid/templates/products/index.mako @@ -38,7 +38,7 @@ diff --git a/rattail/pyramid/views/departments.py b/rattail/pyramid/views/departments.py index a45c6a6c..8f6c9d5b 100644 --- a/rattail/pyramid/views/departments.py +++ b/rattail/pyramid/views/departments.py @@ -26,80 +26,92 @@ ``rattail.pyramid.views.departments`` -- Department Views """ -import transaction -from pyramid.httpexceptions import HTTPFound -from pyramid.view import view_config +# import transaction +# from pyramid.httpexceptions import HTTPFound +# from pyramid.view import view_config -from edbob.pyramid import filters -from edbob.pyramid import forms -from edbob.pyramid import grids -from edbob.pyramid import Session +# from edbob.pyramid import Session +from edbob.pyramid.views import SearchableAlchemyGridView, AlchemyGridView import rattail -@view_config(route_name='departments.list', renderer='/departments/index.mako') -def list_departments(context, request): +# @view_config(route_name='department.delete') +# def delete_department(context, request): +# uuid = request.matchdict['uuid'] +# dept = Session.query(rattail.Department).get(uuid) if uuid else None +# assert dept +# with transaction.manager: +# q = Session.query(rattail.Product) +# q = q.filter(rattail.Product.department_uuid == dept.uuid) +# if q.count(): +# q.update({'department_uuid': None}, synchronize_session=False) +# Session.delete(dept) +# return HTTPFound(location=request.route_url('departments.list')) - fmap = filters.get_filter_map( - rattail.Department, - exact=['number'], - ilike=['name']) - config = filters.get_search_config( - 'departments.list', request, fmap, - include_filter_name=True, - filter_type_name='lk') +class DepartmentsGrid(SearchableAlchemyGridView): - search = filters.get_search_form(config) + mapped_class = rattail.Department + route_name = 'departments' + route_url = '/departments' + renderer = '/departments/index.mako' + permission = 'departments.list' + sort = 'name' - config = grids.get_grid_config( - 'departments.list', request, search, - filter_map=fmap, sort='name', deletable=True) + def filter_map(self): + return self.make_filter_map(ilike=['name']) - smap = grids.get_sort_map( - rattail.Department, - ['number', 'name']) + def filter_config(self): + return self.make_filter_config( + include_filter_name=True, + filter_type_name='lk') - def query(config): - q = Session.query(rattail.Department) - q = filters.filter_query(q, config) - q = grids.sort_query(q, config, smap) + def sort_map(self): + return self.make_sort_map('number', 'name') + + def grid(self): + g = self.make_grid() + g.configure( + include=[ + g.number, + g.name, + ], + readonly=True) + return g + + +class DepartmentsByVendorGrid(AlchemyGridView): + + mapped_class = rattail.Department + route_name = 'departments.by_vendor' + route_url = '/departments/by-vendor' + checkboxes = True + partial_only = True + + def query(self): + q = self.make_query() + q = q.outerjoin(rattail.Product) + q = q.join(rattail.ProductCost) + q = q.join(rattail.Vendor) + q = q.filter(rattail.Vendor.uuid == self.request.params['uuid']) + q = q.distinct() + q = q.order_by(rattail.Department.name) return q - departments = grids.get_pager(query, config) - g = forms.AlchemyGrid( - rattail.Department, departments, config, - gridurl=request.route_url('departments.list'), - delurl='department.delete', - ) - - g.configure( - include=[ - g.number, - g.name, - ], - readonly=True) - - grid = g.render(class_='hoverable departments') - return grids.render_grid(request, grid, search) - - -@view_config(route_name='department.delete') -def delete_department(context, request): - uuid = request.matchdict['uuid'] - dept = Session.query(rattail.Department).get(uuid) if uuid else None - assert dept - with transaction.manager: - q = Session.query(rattail.Product) - q = q.filter(rattail.Product.department_uuid == dept.uuid) - if q.count(): - q.update({'department_uuid': None}, synchronize_session=False) - Session.delete(dept) - return HTTPFound(location=request.route_url('departments.list')) + def grid(self): + g = self.make_grid() + g.configure( + include=[ + g.name, + ], + readonly=True) + return g def includeme(config): - config.add_route('departments.list', '/departments') - config.add_route('department.delete', '/department/{uuid}/delete') - config.scan(__name__) + # config.add_route('department.delete', '/department/{uuid}/delete') + # config.scan(__name__) + + DepartmentsGrid.add_route(config) + DepartmentsByVendorGrid.add_route(config) diff --git a/rattail/pyramid/views/employees.py b/rattail/pyramid/views/employees.py index cf967b30..494dae8d 100644 --- a/rattail/pyramid/views/employees.py +++ b/rattail/pyramid/views/employees.py @@ -26,64 +26,64 @@ ``rattail.pyramid.views.employees`` -- Employee Views """ +from sqlalchemy import and_ + import edbob -from edbob.pyramid.filters import filter_ilike +from edbob.pyramid import grids from edbob.pyramid.forms import AssociationProxyField -from edbob.pyramid.grids import sorter -from edbob.pyramid.views import GridView +from edbob.pyramid.views import SearchableAlchemyGridView from edbob.pyramid.views.crud import Crud import rattail -class EmployeeGrid(GridView): +class EmployeesGrid(SearchableAlchemyGridView): mapped_class = rattail.Employee - route_name = 'employees.list' - route_prefix = 'employee' + route_name = 'employees' + route_url = '/employees' + renderer = '/employees/index.mako' + sort = 'first_name' def filter_map(self): return self.make_filter_map( - first_name=filter_ilike(edbob.Person.first_name), - last_name=filter_ilike(edbob.Person.last_name)) + first_name=grids.search.filter_ilike(edbob.Person.first_name), + last_name=grids.search.filter_ilike(edbob.Person.last_name), + phone=grids.search.filter_ilike(edbob.PersonPhone.number)) - def search_config(self, fmap): - return self.make_search_config( - fmap, + def filter_config(self): + return self.make_filter_config( include_filter_first_name=True, filter_type_first_name='lk', include_filter_last_name=True, - filter_type_last_name='lk') - - def grid_config(self, search, fmap): - kwargs = {} - if self.request.has_perm('employees.delete'): - kwargs['deletable'] = True - return self.make_grid_config( - search, fmap, - sort='first_name', **kwargs) + filter_type_last_name='lk', + filter_label_phone="Phone Number") def sort_map(self): return self.make_sort_map( - first_name=sorter(edbob.Person.first_name), - last_name=sorter(edbob.Person.last_name)) + first_name=grids.util.sorter(edbob.Person.first_name), + last_name=grids.util.sorter(edbob.Person.last_name), + phone=grids.util.sorter(edbob.PersonPhone.number)) - def query(self, config): - q = self.make_query(config) + def query(self): + q = self.make_query() q = q.join(edbob.Person) + q = q.outerjoin(edbob.PersonPhone, and_( + edbob.PersonPhone.parent_uuid == rattail.Employee.person_uuid, + edbob.PersonPhone.preference == 1)) if not self.request.has_perm('employees.edit'): q = q.filter(rattail.Employee.status == rattail.EMPLOYEE_STATUS_CURRENT) return q - def grid(self, data, config): - g = self.make_grid(data, config) + def grid(self): + g = self.make_grid() g.append(AssociationProxyField('first_name')) g.append(AssociationProxyField('last_name')) g.configure( include=[ g.first_name, g.last_name, - # g.status, + g.phone.label("Phone Number"), ], readonly=True) return g @@ -104,5 +104,5 @@ class EmployeeCrud(Crud): def includeme(config): - EmployeeGrid.add_route(config, 'employees.list', '/employees') + EmployeesGrid.add_route(config) EmployeeCrud.add_routes(config) diff --git a/rattail/pyramid/views/products.py b/rattail/pyramid/views/products.py index 0f719a50..77605d49 100644 --- a/rattail/pyramid/views/products.py +++ b/rattail/pyramid/views/products.py @@ -30,24 +30,24 @@ from webhelpers.html.tags import link_to from sqlalchemy.orm import joinedload -from edbob.pyramid.filters import filter_ilike -from edbob.pyramid.grids import sorter -from edbob.pyramid.views import GridView -from edbob.pyramid.views.crud import Crud - import edbob from edbob.pyramid import Session +from edbob.pyramid import grids +from edbob.pyramid.views import SearchableAlchemyGridView +from edbob.pyramid.views.crud import Crud import rattail import rattail.labels from rattail.pyramid.forms import UpcFieldRenderer, PriceFieldRenderer -class ProductGrid(GridView): +class ProductsGrid(SearchableAlchemyGridView): mapped_class = rattail.Product - route_name = 'products.list' - route_prefix = 'product' + route_name = 'products' + route_url = '/products' + renderer = '/products/index.mako' + sort = 'description' def join_map(self): return { @@ -71,15 +71,15 @@ class ProductGrid(GridView): return self.make_filter_map( exact=['upc'], ilike=['description', 'size'], - brand=filter_ilike(rattail.Brand.name), - department=filter_ilike(rattail.Department.name), - subdepartment=filter_ilike(rattail.Subdepartment.name)) + brand=grids.search.filter_ilike(rattail.Brand.name), + department=grids.search.filter_ilike(rattail.Department.name), + subdepartment=grids.search.filter_ilike(rattail.Subdepartment.name)) - def search_config(self, fmap): - return self.make_search_config( - fmap, + def filter_config(self): + return self.make_filter_config( include_filter_upc=True, filter_type_upc='eq', + filter_label_upc="UPC", include_filter_brand=True, filter_type_brand='lk', include_filter_description=True, @@ -87,37 +87,26 @@ class ProductGrid(GridView): include_filter_department=True, filter_type_department='lk') - def search_form(self, config): - return self.make_search_form( - config, upc="UPC") - - def grid_config(self, search, fmap): - kwargs = {} - if self.request.has_perm('products.delete'): - kwargs['deletable'] = True - return self.make_grid_config( - search, fmap, sort='description', **kwargs) - def sort_map(self): return self.make_sort_map( 'upc', 'description', 'size', - brand=sorter(rattail.Brand.name), - department=sorter(rattail.Department.name), - subdepartment=sorter(rattail.Subdepartment.name), - regular_price=sorter(rattail.ProductPrice.price), - current_price=sorter(rattail.ProductPrice.price)) + brand=grids.util.sorter(rattail.Brand.name), + department=grids.util.sorter(rattail.Department.name), + subdepartment=grids.util.sorter(rattail.Subdepartment.name), + regular_price=grids.util.sorter(rattail.ProductPrice.price), + current_price=grids.util.sorter(rattail.ProductPrice.price)) - def query(self, config): - q = self.make_query(config) + def query(self): + q = self.make_query() + q = q.options(joinedload(rattail.Product.brand)) q = q.options(joinedload(rattail.Product.department)) q = q.options(joinedload(rattail.Product.subdepartment)) - q = q.options(joinedload(rattail.Product.brand)) q = q.options(joinedload(rattail.Product.regular_price)) q = q.options(joinedload(rattail.Product.current_price)) return q - def grid(self, data, config): - g = self.make_grid(data, config) + def grid(self): + g = self.make_grid() g.upc.set(renderer=UpcFieldRenderer) g.regular_price.set(renderer=PriceFieldRenderer) g.current_price.set(renderer=PriceFieldRenderer) @@ -127,7 +116,6 @@ class ProductGrid(GridView): g.brand, g.description, g.size, - # g.department, g.subdepartment, g.regular_price.label("Reg. Price"), g.current_price.label("Cur. Price"), @@ -175,7 +163,7 @@ def print_label(request): def includeme(config): - ProductGrid.add_route(config, 'products.list', '/products') + ProductsGrid.add_route(config) ProductCrud.add_routes(config) config.add_route('products.print_label', '/products/label') diff --git a/rattail/pyramid/views/reports.py b/rattail/pyramid/views/reports.py new file mode 100644 index 00000000..8556e874 --- /dev/null +++ b/rattail/pyramid/views/reports.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python + +""" +``dtail.views.reports`` -- Report Views +""" + +import os +import os.path +import re + +from mako.template import Template + +from pyramid.response import Response + +import edbob +from edbob.pyramid import Session + +import rattail + + +def ordering_report(request): + """ + This is the "Ordering Worksheet" report. + """ + + if request.params.get('vendor'): + vendor = Session.query(rattail.Vendor).get(request.params['vendor']) + if vendor: + departments = [] + uuids = request.params.get('departments') + if uuids: + for uuid in uuids.split(','): + dept = Session.query(rattail.Department).get(uuid) + if dept: + departments.append(dept) + body = write_ordering_worksheet(vendor, departments) + response = Response(content_type='text/html') + response.headers['Content-Length'] = len(body) + response.headers['Content-Disposition'] = 'attachment; filename=ordering.html' + response.body = body + return response + return {} + + +def write_ordering_worksheet(vendor, departments): + """ + Rendering engine for the ordering worksheet report. + """ + + q = Session.query(rattail.ProductCost) + q = q.join(rattail.Product) + q = q.filter(rattail.ProductCost.vendor == vendor) + q = q.filter(rattail.Product.department_uuid.in_([x.uuid for x in departments])) + + costs = {} + for cost in q: + dept = cost.product.department + subdept = cost.product.subdepartment + costs.setdefault(dept, {}) + costs[dept].setdefault(subdept, []) + costs[dept][subdept].append(cost) + + plu_upc_pattern = re.compile(r'^0000000(\d{5})$') + weighted_upc_pattern = re.compile(r'^02(\d{5})00000$') + + def get_upc(prod): + upc = '%012u' % prod.upc + m = plu_upc_pattern.match(upc) + if m: + return str(int(m.group(1))) + m = weighted_upc_pattern.match(upc) + if m: + return str(int(m.group(1))) + return upc + + now = edbob.local_time() + data = dict( + vendor=vendor, + costs=costs, + date=now.strftime('%a %d %b %Y'), + time=now.strftime('%I:%M %p'), + get_upc=get_upc, + rattail=rattail, + ) + + report = os.path.join(os.path.dirname(__file__), os.pardir, 'reports', 'ordering_worksheet.mako') + report = os.path.abspath(report) + template = Template(filename=report, disable_unicode=True) + return template.render(**data) + + +def includeme(config): + config.add_route('reports.ordering', '/reports/ordering') + config.add_view(ordering_report, route_name='reports.ordering', renderer='/reports/ordering.mako') diff --git a/rattail/pyramid/views/vendors.py b/rattail/pyramid/views/vendors.py index 6b278bd5..a776df6b 100644 --- a/rattail/pyramid/views/vendors.py +++ b/rattail/pyramid/views/vendors.py @@ -26,48 +26,41 @@ ``rattail.pyramid.views.vendors`` -- Vendor Views """ -from edbob.pyramid.views import GridView, AutocompleteView +from edbob.pyramid.views import SearchableAlchemyGridView, AutocompleteView from edbob.pyramid.views.crud import Crud import rattail -class VendorGrid(GridView): +class VendorsGrid(SearchableAlchemyGridView): mapped_class = rattail.Vendor - route_name = 'vendors.list' - route_prefix = 'vendor' + route_name = 'vendors' + route_url = '/vendors' + renderer = '/vendors/index.mako' + permission = 'vendors.list' + sort = 'name' def filter_map(self): - return self.make_filter_map( - exact=['id'], - ilike=['name']) + return self.make_filter_map(ilike=['name']) - def search_config(self, fmap): - return self.make_search_config( - fmap, + def filter_config(self): + return self.make_filter_config( include_filter_name=True, - filter_type_name='lk') - - def search_form(self, config): - return self.make_search_form(config, id="ID") - - def grid_config(self, search, fmap): - kwargs = {} - if self.request.has_perm('vendors.delete'): - kwargs['deletable'] = True - return self.make_grid_config( - search, fmap, sort='name', **kwargs) + filter_type_name='lk', + filter_label_id="ID") def sort_map(self): return self.make_sort_map('id', 'name') - def grid(self, data, config): - g = self.make_grid(data, config) + def grid(self): + g = self.make_grid() g.configure( include=[ g.id.label("ID"), g.name, + g.phone, + g.contact, ], readonly=True) return g @@ -95,6 +88,6 @@ class VendorAutocomplete(AutocompleteView): def includeme(config): - VendorGrid.add_route(config, 'vendors.list', '/vendors') + VendorsGrid.add_route(config) VendorCrud.add_routes(config) VendorAutocomplete.add_route(config)