Compare commits
No commits in common. "master" and "v0.3a15" have entirely different histories.
548 changed files with 3578 additions and 98566 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -1,8 +1 @@
|
|||
*~
|
||||
*.pyc
|
||||
.coverage
|
||||
.tox/
|
||||
dist/
|
||||
docs/_build/
|
||||
htmlcov/
|
||||
Tailbone.egg-info/
|
||||
rattail.pyramid.egg-info
|
||||
|
|
689
CHANGELOG.md
689
CHANGELOG.md
|
@ -1,689 +0,0 @@
|
|||
|
||||
# Changelog
|
||||
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.8 (2025-05-20)
|
||||
|
||||
### Fix
|
||||
|
||||
- add startup hack for tempmon DB model
|
||||
|
||||
## v0.22.7 (2025-02-19)
|
||||
|
||||
### Fix
|
||||
|
||||
- stop using old config for logo image url on login page
|
||||
- fix warning msg for deprecated Grid param
|
||||
|
||||
## v0.22.6 (2025-02-01)
|
||||
|
||||
### Fix
|
||||
|
||||
- register vue3 form component for products -> make batch
|
||||
|
||||
## v0.22.5 (2024-12-16)
|
||||
|
||||
### Fix
|
||||
|
||||
- whoops this is latest rattail
|
||||
- require newer rattail lib
|
||||
- require newer wuttaweb
|
||||
- let caller request safe HTML literal for rendered grid table
|
||||
|
||||
## v0.22.4 (2024-11-22)
|
||||
|
||||
### Fix
|
||||
|
||||
- avoid error in product search for duplicated key
|
||||
- use vmodel for confirm password widget input
|
||||
|
||||
## v0.22.3 (2024-11-19)
|
||||
|
||||
### Fix
|
||||
|
||||
- avoid error for trainwreck query when not a customer
|
||||
|
||||
## v0.22.2 (2024-11-18)
|
||||
|
||||
### Fix
|
||||
|
||||
- use local/custom enum for continuum operations
|
||||
- add basic master view for Product Costs
|
||||
- show continuum operation type when viewing version history
|
||||
- always define `app` attr for ViewSupplement
|
||||
- avoid deprecated import
|
||||
|
||||
## v0.22.1 (2024-11-02)
|
||||
|
||||
### Fix
|
||||
|
||||
- fix submit button for running problem report
|
||||
- avoid deprecated grid method
|
||||
|
||||
## v0.22.0 (2024-10-22)
|
||||
|
||||
### Feat
|
||||
|
||||
- add support for new ordering batch from parsed file
|
||||
|
||||
### Fix
|
||||
|
||||
- avoid deprecated method to suggest username
|
||||
|
||||
## v0.21.11 (2024-10-03)
|
||||
|
||||
### Fix
|
||||
|
||||
- custom method for adding grid action
|
||||
- become/stop root should redirect to previous url
|
||||
|
||||
## v0.21.10 (2024-09-15)
|
||||
|
||||
### Fix
|
||||
|
||||
- update project repo links, kallithea -> forgejo
|
||||
- use better icon for submit button on login page
|
||||
- wrap notes text for batch view
|
||||
- expose datasync consumer batch size via configure page
|
||||
|
||||
## v0.21.9 (2024-08-28)
|
||||
|
||||
### Fix
|
||||
|
||||
- render custom attrs in form component tag
|
||||
|
||||
## v0.21.8 (2024-08-28)
|
||||
|
||||
### Fix
|
||||
|
||||
- ignore session kwarg for `MasterView.make_row_grid()`
|
||||
|
||||
## v0.21.7 (2024-08-28)
|
||||
|
||||
### Fix
|
||||
|
||||
- avoid error when form value cannot be obtained
|
||||
|
||||
## v0.21.6 (2024-08-28)
|
||||
|
||||
### Fix
|
||||
|
||||
- avoid error when grid value cannot be obtained
|
||||
|
||||
## v0.21.5 (2024-08-28)
|
||||
|
||||
### Fix
|
||||
|
||||
- set empty string for "-new-" file configure option
|
||||
|
||||
## v0.21.4 (2024-08-26)
|
||||
|
||||
### Fix
|
||||
|
||||
- handle differing email profile keys for appinfo/configure
|
||||
|
||||
## v0.21.3 (2024-08-26)
|
||||
|
||||
### Fix
|
||||
|
||||
- show non-standard config values for app info configure email
|
||||
|
||||
## v0.21.2 (2024-08-26)
|
||||
|
||||
### Fix
|
||||
|
||||
- refactor waterpark base template to use wutta feedback component
|
||||
- fix input/output file upload feature for configure pages, per oruga
|
||||
- tweak how grid data translates to Vue template context
|
||||
- merge filters into main grid template
|
||||
- add basic wutta view for users
|
||||
- some fixes for wutta people view
|
||||
- various fixes for waterpark theme
|
||||
- avoid deprecated `component` form kwarg
|
||||
|
||||
## v0.21.1 (2024-08-22)
|
||||
|
||||
### Fix
|
||||
|
||||
- misc. bugfixes per recent changes
|
||||
|
||||
## v0.21.0 (2024-08-22)
|
||||
|
||||
### Feat
|
||||
|
||||
- move "most" filtering logic for grid class to wuttaweb
|
||||
- inherit from wuttaweb templates for home, login pages
|
||||
- inherit from wuttaweb for AppInfoView, appinfo/configure template
|
||||
- add "has output file templates" config option for master view
|
||||
|
||||
### Fix
|
||||
|
||||
- change grid reset-view param name to match wuttaweb
|
||||
- move "searchable columns" grid feature to wuttaweb
|
||||
- use wuttaweb to get/render csrf token
|
||||
- inherit from wuttaweb for appinfo/index template
|
||||
- prefer wuttaweb config for "home redirect to login" feature
|
||||
- fix master/index template rendering for waterpark theme
|
||||
- fix spacing for navbar logo/title in waterpark theme
|
||||
|
||||
## v0.20.1 (2024-08-20)
|
||||
|
||||
### Fix
|
||||
|
||||
- fix default filter verbs logic for workorder status
|
||||
|
||||
## v0.20.0 (2024-08-20)
|
||||
|
||||
### Feat
|
||||
|
||||
- add new 'waterpark' theme, based on wuttaweb w/ vue2 + buefy
|
||||
- refactor templates to simplify base/page/form structure
|
||||
|
||||
### Fix
|
||||
|
||||
- avoid deprecated reference to app db engine
|
||||
|
||||
## v0.19.3 (2024-08-19)
|
||||
|
||||
### Fix
|
||||
|
||||
- add pager stats to all grid vue data (fixes view history)
|
||||
|
||||
## v0.19.2 (2024-08-19)
|
||||
|
||||
### Fix
|
||||
|
||||
- sort on frontend for appinfo package listing grid
|
||||
- prefer attr over key lookup when getting model values
|
||||
- replace all occurrences of `component_studly` => `vue_component`
|
||||
|
||||
## v0.19.1 (2024-08-19)
|
||||
|
||||
### Fix
|
||||
|
||||
- fix broken user auth for web API app
|
||||
|
||||
## v0.19.0 (2024-08-18)
|
||||
|
||||
### Feat
|
||||
|
||||
- move multi-column grid sorting logic to wuttaweb
|
||||
- move single-column grid sorting logic to wuttaweb
|
||||
|
||||
### Fix
|
||||
|
||||
- fix misc. errors in grid template per wuttaweb
|
||||
- fix broken permission directives in web api startup
|
||||
|
||||
## v0.18.0 (2024-08-16)
|
||||
|
||||
### Feat
|
||||
|
||||
- move "basic" grid pagination logic to wuttaweb
|
||||
- inherit from wutta base class for Grid
|
||||
- inherit most logic from wuttaweb, for GridAction
|
||||
|
||||
### Fix
|
||||
|
||||
- avoid route error in user view, when using wutta people view
|
||||
- fix some more wutta compat for base template
|
||||
|
||||
## v0.17.0 (2024-08-15)
|
||||
|
||||
### Feat
|
||||
|
||||
- use wuttaweb for `get_liburl()` logic
|
||||
|
||||
## v0.16.1 (2024-08-15)
|
||||
|
||||
### Fix
|
||||
|
||||
- improve wutta People view a bit
|
||||
- update references to `get_class_hierarchy()`
|
||||
- tweak template for `people/view_profile` per wutta compat
|
||||
|
||||
## v0.16.0 (2024-08-15)
|
||||
|
||||
### Feat
|
||||
|
||||
- add first wutta-based master, for PersonView
|
||||
- refactor forms/grids/views/templates per wuttaweb compat
|
||||
|
||||
## v0.15.6 (2024-08-13)
|
||||
|
||||
### Fix
|
||||
|
||||
- avoid `before_render` subscriber hook for web API
|
||||
- simplify verbiage for batch execution panel
|
||||
|
||||
## v0.15.5 (2024-08-09)
|
||||
|
||||
### Fix
|
||||
|
||||
- assign convenience attrs for all views (config, app, enum, model)
|
||||
|
||||
## v0.15.4 (2024-08-09)
|
||||
|
||||
### Fix
|
||||
|
||||
- avoid bug when checking current theme
|
||||
|
||||
## v0.15.3 (2024-08-08)
|
||||
|
||||
### Fix
|
||||
|
||||
- fix timepicker `parseTime()` when value is null
|
||||
|
||||
## v0.15.2 (2024-08-06)
|
||||
|
||||
### Fix
|
||||
|
||||
- use auth handler, avoid legacy calls for role/perm checks
|
||||
|
||||
## v0.15.1 (2024-08-05)
|
||||
|
||||
### Fix
|
||||
|
||||
- move magic `b` template context var to wuttaweb
|
||||
|
||||
## v0.15.0 (2024-08-05)
|
||||
|
||||
### Feat
|
||||
|
||||
- move more subscriber logic to wuttaweb
|
||||
|
||||
### Fix
|
||||
|
||||
- use wuttaweb logic for `util.get_form_data()`
|
||||
|
||||
## v0.14.5 (2024-08-03)
|
||||
|
||||
### Fix
|
||||
|
||||
- use auth handler instead of deprecated auth functions
|
||||
- avoid duplicate `partial` param when grid reloads data
|
||||
|
||||
## v0.14.4 (2024-07-18)
|
||||
|
||||
### Fix
|
||||
|
||||
- fix more settings persistence bug(s) for datasync/configure
|
||||
- fix modals for luigi tasks page, per oruga
|
||||
|
||||
## v0.14.3 (2024-07-17)
|
||||
|
||||
### Fix
|
||||
|
||||
- fix auto-collapse title for viewing trainwreck txn
|
||||
- allow auto-collapse of header when viewing trainwreck txn
|
||||
|
||||
## v0.14.2 (2024-07-15)
|
||||
|
||||
### Fix
|
||||
|
||||
- add null menu handler, for use with API apps
|
||||
|
||||
## v0.14.1 (2024-07-14)
|
||||
|
||||
### Fix
|
||||
|
||||
- update usage of auth handler, per rattail changes
|
||||
- fix model reference in menu handler
|
||||
- fix bug when making "integration" menus
|
||||
|
||||
## v0.14.0 (2024-07-14)
|
||||
|
||||
### Feat
|
||||
|
||||
- move core menu logic to wuttaweb
|
||||
|
||||
## v0.13.2 (2024-07-13)
|
||||
|
||||
### Fix
|
||||
|
||||
- fix logic bug for datasync/config settings save
|
||||
|
||||
## v0.13.1 (2024-07-13)
|
||||
|
||||
### Fix
|
||||
|
||||
- fix settings persistence bug(s) for datasync/configure page
|
||||
|
||||
## v0.13.0 (2024-07-12)
|
||||
|
||||
### Feat
|
||||
|
||||
- begin integrating WuttaWeb as upstream dependency
|
||||
|
||||
### Fix
|
||||
|
||||
- cast enum as list to satisfy deform widget
|
||||
|
||||
## v0.12.1 (2024-07-11)
|
||||
|
||||
### Fix
|
||||
|
||||
- refactor `config.get_model()` => `app.model`
|
||||
|
||||
## v0.12.0 (2024-07-09)
|
||||
|
||||
### Feat
|
||||
|
||||
- drop python 3.6 support, use pyproject.toml (again)
|
||||
|
||||
## v0.11.10 (2024-07-05)
|
||||
|
||||
### Fix
|
||||
|
||||
- make the Members tab optional, for profile view
|
||||
|
||||
## v0.11.9 (2024-07-05)
|
||||
|
||||
### Fix
|
||||
|
||||
- do not show flash message when changing app theme
|
||||
|
||||
- improve collapse panels for butterball theme
|
||||
|
||||
- expand input for butterball theme
|
||||
|
||||
- add xref button to customer profile, for trainwreck txn view
|
||||
|
||||
- add optional Transactions tab for profile view
|
||||
|
||||
## v0.11.8 (2024-07-04)
|
||||
|
||||
### Fix
|
||||
|
||||
- fix grid action icons for datasync/configure, per oruga
|
||||
|
||||
- allow view supplements to add extra links for profile employee tab
|
||||
|
||||
- leverage import handler method to determine command/subcommand
|
||||
|
||||
- add tool to make user account from profile view
|
||||
|
||||
## v0.11.7 (2024-07-04)
|
||||
|
||||
### Fix
|
||||
|
||||
- add stacklevel to deprecation warnings
|
||||
|
||||
- require zope.sqlalchemy >= 1.5
|
||||
|
||||
- include edit profile email/phone dialogs only if user has perms
|
||||
|
||||
- allow view supplements to add to profile member context
|
||||
|
||||
- cast enum as list to satisfy deform widget
|
||||
|
||||
- expand POD image URL setting input
|
||||
|
||||
## v0.11.6 (2024-07-01)
|
||||
|
||||
### Fix
|
||||
|
||||
- set explicit referrer when changing dbkey
|
||||
|
||||
- remove references, dependency for `six` package
|
||||
|
||||
## v0.11.5 (2024-06-30)
|
||||
|
||||
### Fix
|
||||
|
||||
- allow comma in numeric filter input
|
||||
|
||||
- add custom url prefix if needed, for fanstatic
|
||||
|
||||
- use vue 3.4.31 and oruga 0.8.12 by default
|
||||
|
||||
## v0.11.4 (2024-06-30)
|
||||
|
||||
### Fix
|
||||
|
||||
- start/stop being root should submit POST instead of GET
|
||||
|
||||
- require vendor when making new ordering batch via api
|
||||
|
||||
- don't escape each address for email attempts grid
|
||||
|
||||
## v0.11.3 (2024-06-28)
|
||||
|
||||
### Fix
|
||||
|
||||
- add link to "resolved by" user for pending products
|
||||
|
||||
- handle error when merging 2 records fails
|
||||
|
||||
## v0.11.2 (2024-06-18)
|
||||
|
||||
### Fix
|
||||
|
||||
- hide certain custorder settings if not applicable
|
||||
|
||||
- use different logic for buefy/oruga for product lookup keydown
|
||||
|
||||
- product records should be touchable
|
||||
|
||||
- show flash error message if resolve pending product fails
|
||||
|
||||
## v0.11.1 (2024-06-14)
|
||||
|
||||
### Fix
|
||||
|
||||
- revert back to setup.py + setup.cfg
|
||||
|
||||
## v0.11.0 (2024-06-10)
|
||||
|
||||
### Feat
|
||||
|
||||
- switch from setup.cfg to pyproject.toml + hatchling
|
||||
|
||||
## v0.10.16 (2024-06-10)
|
||||
|
||||
### Feat
|
||||
|
||||
- standardize how app, package versions are determined
|
||||
|
||||
### Fix
|
||||
|
||||
- avoid deprecated config methods for app/node title
|
||||
|
||||
## v0.10.15 (2024-06-07)
|
||||
|
||||
### Fix
|
||||
|
||||
- do *not* Use `pkg_resources` to determine package versions
|
||||
|
||||
## v0.10.14 (2024-06-06)
|
||||
|
||||
### Fix
|
||||
|
||||
- use `pkg_resources` to determine package versions
|
||||
|
||||
## v0.10.13 (2024-06-06)
|
||||
|
||||
### Feat
|
||||
|
||||
- remove old/unused scaffold for use with `pcreate`
|
||||
|
||||
- add 'fanstatic' support for sake of libcache assets
|
||||
|
||||
## v0.10.12 (2024-06-04)
|
||||
|
||||
### Feat
|
||||
|
||||
- require pyramid 2.x; remove 1.x-style auth policies
|
||||
|
||||
- remove version cap for deform
|
||||
|
||||
- set explicit referrer when changing app theme
|
||||
|
||||
- add `<b-tooltip>` component shim
|
||||
|
||||
- include extra styles from `base_meta` template for butterball
|
||||
|
||||
- include butterball theme by default for new apps
|
||||
|
||||
### Fix
|
||||
|
||||
- fix product lookup component, per butterball
|
||||
|
||||
## v0.10.11 (2024-06-03)
|
||||
|
||||
### Feat
|
||||
|
||||
- fix vue3 refresh bugs for various views
|
||||
|
||||
- fix grid bug for tempmon appliance view, per oruga
|
||||
|
||||
- fix ordering worksheet generator, per butterball
|
||||
|
||||
- fix inventory worksheet generator, per butterball
|
||||
|
||||
## v0.10.10 (2024-06-03)
|
||||
|
||||
### Feat
|
||||
|
||||
- more butterball fixes for "view profile" template
|
||||
|
||||
### Fix
|
||||
|
||||
- fix focus for `<b-select>` shim component
|
||||
|
||||
## v0.10.9 (2024-06-03)
|
||||
|
||||
### Feat
|
||||
|
||||
- let master view control context menu items for page
|
||||
|
||||
- fix the "new custorder" page for butterball
|
||||
|
||||
### Fix
|
||||
|
||||
- fix panel style for PO vs. Invoice breakdown in receiving batch
|
||||
|
||||
## v0.10.8 (2024-06-02)
|
||||
|
||||
### Feat
|
||||
|
||||
- add styling for checked grid rows, per oruga/butterball
|
||||
|
||||
- fix product view template for oruga/butterball
|
||||
|
||||
- allow per-user custom styles for butterball
|
||||
|
||||
- use oruga 0.8.9 by default
|
||||
|
||||
## v0.10.7 (2024-06-01)
|
||||
|
||||
### Feat
|
||||
|
||||
- add setting to allow decimal quantities for receiving
|
||||
|
||||
- log error if registry has no rattail config
|
||||
|
||||
- add column filters for import/export main grid
|
||||
|
||||
- escape all unsafe html for grid data
|
||||
|
||||
- add speedbumps for delete, set preferred email/phone in profile view
|
||||
|
||||
- fix file upload widget for oruga
|
||||
|
||||
### Fix
|
||||
|
||||
- fix overflow when instance header title is too long (butterball)
|
||||
|
||||
## v0.10.6 (2024-05-29)
|
||||
|
||||
### Feat
|
||||
|
||||
- add way to flag organic products within lookup dialog
|
||||
|
||||
- expose db picker for butterball theme
|
||||
|
||||
- expose quickie lookup for butterball theme
|
||||
|
||||
- fix basic problems with people profile view, per butterball
|
||||
|
||||
## v0.10.5 (2024-05-29)
|
||||
|
||||
### Feat
|
||||
|
||||
- add `<tailbone-timepicker>` component for oruga
|
||||
|
||||
## v0.10.4 (2024-05-12)
|
||||
|
||||
### Fix
|
||||
|
||||
- fix styles for grid actions, per butterball
|
||||
|
||||
## v0.10.3 (2024-05-10)
|
||||
|
||||
### Fix
|
||||
|
||||
- fix bug with grid date filters
|
||||
|
||||
## v0.10.2 (2024-05-08)
|
||||
|
||||
### Feat
|
||||
|
||||
- remove version restriction for pyramid_beaker dependency
|
||||
|
||||
- rename some attrs etc. for buefy components used with oruga
|
||||
|
||||
- fix "tools" helper for receiving batch view, per oruga
|
||||
|
||||
- more data type fixes for ``<tailbone-datepicker>``
|
||||
|
||||
- fix "view receiving row" page, per oruga
|
||||
|
||||
- tweak styles for grid action links, per butterball
|
||||
|
||||
### Fix
|
||||
|
||||
- fix employees grid when viewing department (per oruga)
|
||||
|
||||
- fix login "enter" key behavior, per oruga
|
||||
|
||||
- fix button text for autocomplete
|
||||
|
||||
## v0.10.1 (2024-04-28)
|
||||
|
||||
### Feat
|
||||
|
||||
- sort list of available themes
|
||||
|
||||
- update various icon names for oruga compatibility
|
||||
|
||||
- show "View This" button when cloning a record
|
||||
|
||||
- stop including 'falafel' as available theme
|
||||
|
||||
### Fix
|
||||
|
||||
- fix vertical alignment in main menu bar, for butterball
|
||||
|
||||
- fix upgrade execution logic/UI per oruga
|
||||
|
||||
## v0.10.0 (2024-04-28)
|
||||
|
||||
This version bump is to reflect adding support for Vue 3 + Oruga via
|
||||
the 'butterball' theme. There is likely more work to be done for that
|
||||
yet, but it mostly works at this point.
|
||||
|
||||
### Feat
|
||||
|
||||
- misc. template and view logic tweaks (applicable to all themes) for
|
||||
better patterns, consistency etc.
|
||||
|
||||
- add initial support for Vue 3 + Oruga, via "butterball" theme
|
||||
|
||||
|
||||
## Older Releases
|
||||
|
||||
Please see `docs/OLDCHANGES.rst` for older release notes.
|
130
CHANGES.txt
Normal file
130
CHANGES.txt
Normal file
|
@ -0,0 +1,130 @@
|
|||
|
||||
0.3a15
|
||||
------
|
||||
|
||||
- [bug] Fixed batch grid and CRUD views so that the execution time shows a
|
||||
pretty (and local) display instead of 24-hour UTC time.
|
||||
|
||||
0.3a14
|
||||
------
|
||||
|
||||
- [feature] Added some more CRUD. Mostly this was for departments,
|
||||
subdepartments, brands and products. This was rather ad-hoc and still is
|
||||
probably far from complete.
|
||||
|
||||
- [general] Changed main batch route.
|
||||
|
||||
- [bug] Fixed label profile templates so they properly handle a missing or
|
||||
invalid printer spec.
|
||||
|
||||
0.3a13
|
||||
------
|
||||
|
||||
- [bug] Fixed bug which prevented UPC search from working on products screen.
|
||||
|
||||
0.3a12
|
||||
------
|
||||
|
||||
- [general] Fixed namespace packages, per ``setuptools`` documentation.
|
||||
|
||||
- [feature] Added support for ``LabelProfile.visible``. This field may now be
|
||||
edited, and it is honored when displaying the list of available profiles to
|
||||
be used for printing from the products page.
|
||||
|
||||
- [bug] Fixed bug where non-numeric data entered in the UPC search field on the
|
||||
products page was raising an error.
|
||||
|
||||
0.3a11
|
||||
------
|
||||
|
||||
- [bug] Fixed product label printing to handle any uncaught exception, and
|
||||
report the error message to the end user.
|
||||
|
||||
0.3a10
|
||||
------
|
||||
|
||||
- [general] Updated category views and templates. These were sorely out of
|
||||
date.
|
||||
|
||||
0.3a9
|
||||
-----
|
||||
|
||||
- Add brands autocomplete view.
|
||||
|
||||
- Add departments autocomplete view.
|
||||
|
||||
- Add ID filter to vendors grid.
|
||||
|
||||
0.3a8
|
||||
-----
|
||||
|
||||
- Tweak batch progress indicators.
|
||||
|
||||
- Add "Executed" column, filter to batch grid.
|
||||
|
||||
0.3a7
|
||||
-----
|
||||
|
||||
- Add ability to restrict batch providers via config.
|
||||
|
||||
0.3a6
|
||||
-----
|
||||
|
||||
- Add Vendor CRUD.
|
||||
|
||||
- Add Brand views.
|
||||
|
||||
0.3a5
|
||||
-----
|
||||
|
||||
- Added support for GPC data type.
|
||||
|
||||
- Added eager import of ``rattail.sil`` in ``before_render`` hook.
|
||||
|
||||
- Removed ``rattail.pyramid.util`` module.
|
||||
|
||||
- Added initial batch support: views, templates, creation from Product grid.
|
||||
|
||||
- Added support for ``rattail.LabelProfile`` class.
|
||||
|
||||
- Improved Product grid to include filter/sort on Vendor.
|
||||
|
||||
- Cleaned up dependencies.
|
||||
|
||||
- Added ``rattail.pyramid.includeme()``.
|
||||
|
||||
- Added ``CustomerGroup`` CRUD view (read only).
|
||||
|
||||
- Added hot links to ``Customer`` CRUD view.
|
||||
|
||||
- Added ``Store`` index, CRUD views.
|
||||
|
||||
- Updated ``rattail.pyramid.views.includeme()``.
|
||||
|
||||
- Added ``email_preference`` to ``Customer`` CRUD.
|
||||
|
||||
0.3a4
|
||||
-----
|
||||
|
||||
- Update grid and CRUD views per changes in ``edbob``.
|
||||
|
||||
0.3a3
|
||||
-----
|
||||
|
||||
- Add price field renderers.
|
||||
|
||||
- Add/tweak lots of views for database models.
|
||||
|
||||
- Add label printing to product list view.
|
||||
|
||||
- Add (some of) ``Product`` CRUD.
|
||||
|
||||
0.3a2
|
||||
-----
|
||||
|
||||
- Refactor category views.
|
||||
|
||||
0.3a1
|
||||
-----
|
||||
|
||||
- Initial port to Rattail v0.3.
|
141
COPYING.txt
141
COPYING.txt
|
@ -1,5 +1,5 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
|
@ -7,17 +7,15 @@
|
|||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
|
@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
|||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
@ -72,7 +60,7 @@ modification follow.
|
|||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
|||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
|
@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
|
|
20
MANIFEST.in
20
MANIFEST.in
|
@ -1,18 +1,2 @@
|
|||
|
||||
include *.txt
|
||||
include *.rst
|
||||
include *.py
|
||||
|
||||
include tailbone/static/robots.txt
|
||||
recursive-include tailbone/static *.js
|
||||
recursive-include tailbone/static *.css
|
||||
recursive-include tailbone/static *.png
|
||||
recursive-include tailbone/static *.jpg
|
||||
recursive-include tailbone/static *.gif
|
||||
recursive-include tailbone/static *.ico
|
||||
|
||||
recursive-include tailbone/static/files *
|
||||
|
||||
recursive-include tailbone/templates *.mako
|
||||
recursive-include tailbone/templates *.pt
|
||||
recursive-include tailbone/reports *.mako
|
||||
include *.txt *.ini *.cfg *.rst
|
||||
recursive-include rattail/pyramid *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
# Tailbone
|
||||
|
||||
Tailbone is an extensible web application based on Rattail. It provides a
|
||||
"back-office network environment" (BONE) for use in managing retail data.
|
||||
|
||||
Please see Rattail's [home page](http://rattailproject.org/) for more
|
||||
information.
|
11
README.txt
Normal file
11
README.txt
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
rattail.pyramid
|
||||
===============
|
||||
|
||||
Rattail is a retail software framework based on `edbob <http://edbob.org/>`_,
|
||||
and released under the GNU Affero General Public License.
|
||||
|
||||
This package contains Pyramid views, etc., for managing a Rattail system.
|
||||
|
||||
Please see Rattail's `home page <http://rattail.edbob.org/>`_ for more
|
||||
information.
|
177
docs/Makefile
177
docs/Makefile
|
@ -1,177 +0,0 @@
|
|||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# User-friendly check for sphinx-build
|
||||
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
||||
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
|
||||
endif
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " xml to make Docutils-native XML files"
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Tailbone.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Tailbone.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/Tailbone"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Tailbone"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
latexpdfja:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||
|
||||
pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
7539
docs/OLDCHANGES.rst
7539
docs/OLDCHANGES.rst
File diff suppressed because it is too large
Load diff
0
docs/_static/.dummy
vendored
0
docs/_static/.dummy
vendored
|
@ -1,15 +0,0 @@
|
|||
|
||||
``tailbone.api.batch.core``
|
||||
===========================
|
||||
|
||||
.. automodule:: tailbone.api.batch.core
|
||||
|
||||
.. autoclass:: APIBatchMixin
|
||||
|
||||
.. autoclass:: APIBatchView
|
||||
|
||||
.. autoclass:: APIBatchRowView
|
||||
|
||||
.. autoattribute:: editable
|
||||
|
||||
.. autoattribute:: supports_quick_entry
|
|
@ -1,41 +0,0 @@
|
|||
|
||||
``tailbone.api.batch.ordering``
|
||||
===============================
|
||||
|
||||
.. automodule:: tailbone.api.batch.ordering
|
||||
|
||||
.. autoclass:: OrderingBatchViews
|
||||
|
||||
.. autoattribute:: collection_url_prefix
|
||||
|
||||
.. autoattribute:: object_url_prefix
|
||||
|
||||
.. autoattribute:: model_class
|
||||
|
||||
.. autoattribute:: route_prefix
|
||||
|
||||
.. autoattribute:: permission_prefix
|
||||
|
||||
.. autoattribute:: default_handler_spec
|
||||
|
||||
.. automethod:: base_query
|
||||
|
||||
.. automethod:: create_object
|
||||
|
||||
.. autoclass:: OrderingBatchRowViews
|
||||
|
||||
.. autoattribute:: collection_url_prefix
|
||||
|
||||
.. autoattribute:: object_url_prefix
|
||||
|
||||
.. autoattribute:: model_class
|
||||
|
||||
.. autoattribute:: route_prefix
|
||||
|
||||
.. autoattribute:: permission_prefix
|
||||
|
||||
.. autoattribute:: default_handler_spec
|
||||
|
||||
.. autoattribute:: supports_quick_entry
|
||||
|
||||
.. automethod:: update_object
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.db``
|
||||
===============
|
||||
|
||||
.. automodule:: tailbone.db
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.diffs``
|
||||
==================
|
||||
|
||||
.. automodule:: tailbone.diffs
|
||||
:members:
|
|
@ -1,9 +0,0 @@
|
|||
|
||||
``tailbone.forms``
|
||||
==================
|
||||
|
||||
.. automodule:: tailbone.forms
|
||||
:members:
|
||||
|
||||
.. autoclass:: tailbone.forms.Form
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.forms.widgets``
|
||||
==========================
|
||||
|
||||
.. automodule:: tailbone.forms.widgets
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.grids.core``
|
||||
=======================
|
||||
|
||||
.. automodule:: tailbone.grids.core
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.grids``
|
||||
==================
|
||||
|
||||
.. automodule:: tailbone.grids
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.progress``
|
||||
=====================
|
||||
|
||||
.. automodule:: tailbone.progress
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.subscribers``
|
||||
========================
|
||||
|
||||
.. automodule:: tailbone.subscribers
|
||||
:members:
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.util``
|
||||
=================
|
||||
|
||||
.. automodule:: tailbone.util
|
||||
:members:
|
|
@ -1,5 +0,0 @@
|
|||
|
||||
``tailbone.views.batch``
|
||||
========================
|
||||
|
||||
.. automodule:: tailbone.views.batch
|
|
@ -1,10 +0,0 @@
|
|||
|
||||
``tailbone.views.batch.vendorcatalog``
|
||||
======================================
|
||||
|
||||
.. automodule:: tailbone.views.batch.vendorcatalog
|
||||
|
||||
.. autoclass:: VendorCatalogsView
|
||||
:members:
|
||||
|
||||
.. autofunction:: includeme
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.views.core``
|
||||
=======================
|
||||
|
||||
.. automodule:: tailbone.views.core
|
||||
:members:
|
|
@ -1,126 +0,0 @@
|
|||
|
||||
``tailbone.views.master``
|
||||
=========================
|
||||
|
||||
.. module:: tailbone.views.master
|
||||
|
||||
Model Master View
|
||||
------------------
|
||||
|
||||
This module contains the "model master" view class. This is a convenience
|
||||
abstraction which provides some patterns/consistency for the typical set of
|
||||
views needed to expose a table's data for viewing/editing/etc. Usually this
|
||||
means providing something like the following view methods for a model:
|
||||
|
||||
* index (list/filter)
|
||||
* create
|
||||
* view
|
||||
* edit
|
||||
* delete
|
||||
|
||||
The actual list of provided view methods will depend on usage. Generally
|
||||
speaking, each view method which is provided by the master class may be
|
||||
configured in some way by the subclass (e.g. add extra filters to a grid).
|
||||
|
||||
.. autoclass:: MasterView
|
||||
|
||||
.. automethod:: index
|
||||
|
||||
.. automethod:: create
|
||||
|
||||
.. automethod:: view
|
||||
|
||||
.. automethod:: edit
|
||||
|
||||
.. automethod:: delete
|
||||
|
||||
Attributes to Override
|
||||
----------------------
|
||||
|
||||
The following is a list of attributes which you can (and in some cases must)
|
||||
override when defining your subclass.
|
||||
|
||||
.. attribute:: MasterView.model_class
|
||||
|
||||
All master view subclasses *must* define this attribute. Its value must
|
||||
be a data model class which has been mapped via SQLAlchemy, e.g.
|
||||
``rattail.db.model.Product``.
|
||||
|
||||
.. attribute:: MasterView.normalized_model_name
|
||||
|
||||
Name of the model class which has been "normalized" for the sake of usage
|
||||
as a key (for grid settings etc.). If not defined by the subclass, the
|
||||
default will be the lower-cased model class name, e.g. 'product'.
|
||||
|
||||
.. attribute:: grid_key
|
||||
|
||||
Unique value to be used as a key for the grid settings, etc. If not
|
||||
defined by the subclass, the normalized model name will be used.
|
||||
|
||||
.. attribute:: MasterView.route_prefix
|
||||
|
||||
Value with which all routes provided by the view class will be prefixed.
|
||||
If not defined by the subclass, a default will be constructed by simply
|
||||
adding an 's' to the end of the normalized model name, e.g. 'products'.
|
||||
|
||||
.. attribute:: MasterView.grid_factory
|
||||
|
||||
Factory callable to be used when creating new grid instances; defaults to
|
||||
:class:`tailbone.grids.Grid`.
|
||||
|
||||
.. attribute:: MasterView.results_downloadable_csv
|
||||
|
||||
Flag indicating whether the view should allow CSV download of grid data,
|
||||
i.e. primary search results.
|
||||
|
||||
.. attribute:: MasterView.help_url
|
||||
|
||||
If set, this defines the "default" help URL for all views provided by the
|
||||
master. Default value for this is simply ``None`` which would mean the
|
||||
Help button is not shown at all. Note that the master may choose to
|
||||
override this for certain views, if so that should be done within
|
||||
:meth:`get_help_url()`.
|
||||
|
||||
.. attribute:: MasterView.version_diff_factory
|
||||
|
||||
Optional factory to use for version diff objects. By default
|
||||
this is *not set* but a subclass is free to set it. See also
|
||||
:meth:`get_version_diff_factory()`.
|
||||
|
||||
|
||||
Methods to Override
|
||||
-------------------
|
||||
|
||||
The following is a list of methods which you can override when defining your
|
||||
subclass.
|
||||
|
||||
.. automethod:: MasterView.editable_instance
|
||||
|
||||
.. .. automethod:: MasterView.get_settings
|
||||
|
||||
.. automethod:: MasterView.get_csv_fields
|
||||
|
||||
.. automethod:: MasterView.get_csv_row
|
||||
|
||||
.. automethod:: MasterView.get_help_url
|
||||
|
||||
.. automethod:: MasterView.get_model_key
|
||||
|
||||
.. automethod:: MasterView.get_version_diff_enums
|
||||
|
||||
.. automethod:: MasterView.get_version_diff_factory
|
||||
|
||||
.. automethod:: MasterView.make_version_diff
|
||||
|
||||
.. automethod:: MasterView.title_for_version
|
||||
|
||||
|
||||
Support Methods
|
||||
---------------
|
||||
|
||||
The following is a list of methods you should (probably) not need to
|
||||
override, but may find useful:
|
||||
|
||||
.. automethod:: MasterView.default_edit_url
|
||||
|
||||
.. automethod:: MasterView.get_action_route_kwargs
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
``tailbone.views.members``
|
||||
==========================
|
||||
|
||||
.. automodule:: tailbone.views.members
|
||||
:members:
|
|
@ -1,9 +0,0 @@
|
|||
|
||||
``tailbone.views.purchasing.batch``
|
||||
===================================
|
||||
|
||||
.. automodule:: tailbone.views.purchasing.batch
|
||||
|
||||
.. autoclass:: PurchasingBatchView
|
||||
|
||||
.. automethod:: save_edit_row_form
|
|
@ -1,15 +0,0 @@
|
|||
|
||||
``tailbone.views.purchasing.ordering``
|
||||
======================================
|
||||
|
||||
.. automodule:: tailbone.views.purchasing.ordering
|
||||
|
||||
.. autoclass:: OrderingBatchView
|
||||
|
||||
.. autoattribute:: model_class
|
||||
|
||||
.. autoattribute:: default_handler_spec
|
||||
|
||||
.. automethod:: configure_row_form
|
||||
|
||||
.. automethod:: worksheet_update
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
Changelog Archive
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
OLDCHANGES
|
|
@ -1,65 +0,0 @@
|
|||
|
||||
Data Batches
|
||||
============
|
||||
|
||||
.. contents:: :local:
|
||||
|
||||
Data "batches" are one of the most powerful features of Rattail / Tailbone.
|
||||
However each "batch type" is different, and they usually require custom
|
||||
development. In all cases they require a Rattail-based app database, for
|
||||
storage.
|
||||
|
||||
|
||||
General Overview
|
||||
----------------
|
||||
|
||||
You can think of data batches as a sort of "temporary spreadsheet" feature.
|
||||
When a batch is created, it is usually populated with rows, from some data
|
||||
source. The user(s) may then manipulate the batch data as needed, with the
|
||||
final goal being to "execute" the batch. What execution specifically means
|
||||
will depend on context, e.g. type of batch, but generally it will "commit" the
|
||||
"pending changes" which are represented by the batch.
|
||||
|
||||
Note that when a batch is executed, it becomes read-only ("frozen in time") and
|
||||
at that point may be considered part of an audit trail of sorts. The utility
|
||||
of this may vary depending on the nature of the batch data.
|
||||
|
||||
Beyond that it's difficult to describe batches very well at this level,
|
||||
precisely because they're all different.
|
||||
|
||||
..
|
||||
This graphic tries to show how batches are created and executed over time.
|
||||
Note that each batch type is free to target a different system(s) upon
|
||||
execution.
|
||||
|
||||
TODO: need graphic
|
||||
|
||||
|
||||
Batch Tables
|
||||
------------
|
||||
|
||||
In most cases the table(s) underlying a particular batch type, have a "static"
|
||||
schema and must be defined as ORM classes, e.g. within the ``poser.db.model``
|
||||
package.
|
||||
|
||||
In some rare cases the batch data (row) table may be dynamic; however the batch
|
||||
header table must still be defined.
|
||||
|
||||
|
||||
Batch Handlers
|
||||
--------------
|
||||
|
||||
Once the batch table(s) are present, the next puzzle piece is the batch
|
||||
handler. Again there is generally (at least) one handler defined for each
|
||||
batch type.
|
||||
|
||||
The batch "handler" is considered part of the data layer and provides logic for
|
||||
populating the batch, executing it etc.
|
||||
|
||||
|
||||
Batch Views
|
||||
-----------
|
||||
|
||||
This discussion would not be complete without mentioning the web views for the
|
||||
batch. Again each batch type will require a custom view(s) although these
|
||||
"usually" are simple wrappers as most logic is provided by the base view.
|
|
@ -1,115 +0,0 @@
|
|||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
.. contents:: :local:
|
||||
|
||||
Configuration for an app can come from two sources: configuration file(s), and
|
||||
the Settings table in the database.
|
||||
|
||||
|
||||
Config File Inheritance
|
||||
-----------------------
|
||||
|
||||
An important thing to understand regarding Rattail config files, is that one
|
||||
file may "include" another file(s), which in turn may "include" others etc.
|
||||
Invocation of the app will often require only a single config file to be
|
||||
specified, since that file may include others as needed.
|
||||
|
||||
For example ``web.conf`` will typically include ``rattail.conf`` but the web
|
||||
app need only be invoked with ``web.conf`` - config from both files will inform
|
||||
the app's behavior.
|
||||
|
||||
|
||||
Typical Config Files
|
||||
--------------------
|
||||
|
||||
A typical Poser (Rattail-based) app will have at the very least, one file named
|
||||
``rattail.conf`` - this is considered the most fundamental config file. It
|
||||
will usually define database connections, logging config, and any other "core"
|
||||
things which would be required for any invocation of the app, regardless of the
|
||||
environment (e.g. console vs. web).
|
||||
|
||||
Note that even ``rattail.conf`` is free to include other files. This may be
|
||||
useful for instance, if you have a single site-wide config file which is shared
|
||||
among all Rattail apps.
|
||||
|
||||
There is no *strict* requirement for having a ``rattail.conf`` file, but these
|
||||
docs will assume its presence. Here are some other typical files, which the
|
||||
docs also may reference occasionally:
|
||||
|
||||
**web.conf** - This is the "core" config file for the web app, although it
|
||||
still includes the ``rattail.conf`` file. In production (running on Apache
|
||||
etc.) it is specified within the WSGI module which is responsible for
|
||||
instantiating the web app. When running the development server, it is
|
||||
specified via command line.
|
||||
|
||||
**quiet.conf** - This is a slight wrapper around ``rattail.conf`` for the sake
|
||||
of a "quieter" console, when running app commands via console. It may be used
|
||||
in place of ``rattail.conf`` - i.e. you would specify ``-c quiet.conf`` when
|
||||
running the command. The only function of this wrapper is to set the level to
|
||||
INFO for the console logging handler. In practice this hides DEBUG logging
|
||||
messages which are shown by default when using ``rattail.conf`` as the app
|
||||
config file.
|
||||
|
||||
**cron.conf** - Another wrapper around ``rattail.conf`` which suppresses
|
||||
logging even further. The idea is that this config file would be used by cron
|
||||
jobs; that way the only actual output is warnings and errors, hence cron would
|
||||
not send email unless something actually went wrong. It may be used in place
|
||||
of ``rattail.conf`` - i.e. you would specify ``-c cron.conf`` when running the
|
||||
command. The only function of this wrapper is to set the level to WARNING for
|
||||
the console logging handler.
|
||||
|
||||
**ignore-changes.conf** - This file is only relevant if your ``rattail.conf``
|
||||
says to "record changes" when write activity occurs in the database(s). Note
|
||||
that this file does *not* include ``rattail.conf`` because it is meant to be
|
||||
supplemental only. For instance on the command line, you would need to specify
|
||||
two config files, first ``rattail.conf`` or a suitable alternative, but then
|
||||
``ignore-changes.conf`` also. If specified, this file will cause changes to be
|
||||
ignored, i.e. **not recorded** when write activity occurs.
|
||||
|
||||
**without-versioning.conf** - This file is only relevant if your
|
||||
``rattail.conf`` says to enable "data versioning" when write activity occurs in
|
||||
the database(s). Note that this file does *not* include ``rattail.conf``
|
||||
because it is meant to be supplemental only. For instance on the command line,
|
||||
you would need to specify two config files, first ``rattail.conf`` or a
|
||||
suitable alternative, but then ``without-versioning.conf`` also. If specified,
|
||||
this file will disable the data versioning system entirely. Note that if
|
||||
versioning is undesirable for a given app run, this is the only way to
|
||||
effectively disable it; once loaded that feature cannot be disabled.
|
||||
|
||||
|
||||
Settings from Database
|
||||
----------------------
|
||||
|
||||
The other (often more convenient) source of app configuration is the Settings
|
||||
table within the app database. Whether or not this table is a valid source for
|
||||
app configuration, ultimately depends on what the config file(s) has to say
|
||||
about it.
|
||||
|
||||
Assuming the config file(s) defines a database connection and declares it a
|
||||
valid source for config values, then the Settings table may contribute to the
|
||||
running app config. The nice thing about this is that these settings are
|
||||
checked in real-time. So whereas changing a config file will require an app
|
||||
restart, any edits to the settings table should take effect immediately.
|
||||
|
||||
Usually the settings table will *override* values found in the config file.
|
||||
This behavior also is configurable to some extent, and in some cases a config
|
||||
value may *only* come from a config file and never the settings table.
|
||||
|
||||
An example may help here. If the config file contained the following value:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[poser]
|
||||
foo = bar
|
||||
|
||||
Then you could create a new Setting in the database with the following fields:
|
||||
|
||||
* **name** = poser.foo
|
||||
* **value** = baz
|
||||
|
||||
Assuming typical setup, i.e. where settings table may override config file, the
|
||||
app would consider 'baz' to be the config value. So basically the setting name
|
||||
must correspond to a combination of the config file "section" name, then a dot,
|
||||
then the "option" name.
|
|
@ -1,7 +0,0 @@
|
|||
|
||||
Console Commands
|
||||
================
|
||||
|
||||
.. contents:: :local:
|
||||
|
||||
TODO
|
|
@ -1,45 +0,0 @@
|
|||
|
||||
Database Schema
|
||||
===============
|
||||
|
||||
.. contents:: :local:
|
||||
|
||||
Rattail provides a "core" schema which is assumed to be the foundation of any
|
||||
Poser app database.
|
||||
|
||||
|
||||
Core Tables
|
||||
-----------
|
||||
|
||||
All tables which are considered part of the Rattail "core" schema, are defined
|
||||
as ORM classes within the ``rattail.db.model`` package.
|
||||
|
||||
.. note::
|
||||
|
||||
The Rattail project has its roots in retail grocery-type stores, and its
|
||||
schema reflects that to a large degree. In practice however the software
|
||||
may be used to support a wide variety of apps. The next section describes
|
||||
that a bit more.
|
||||
|
||||
|
||||
Customizing the Schema
|
||||
----------------------
|
||||
|
||||
Almost certainly a custom app will need some of the core tables, but just as
|
||||
certainly, it will *not* need others. And to make things even more
|
||||
interesting, it may need some tables but also need to "supplement" them
|
||||
somehow, to track additional data for each record etc.
|
||||
|
||||
Any table in the core schema which is *not* needed, may simply be ignored,
|
||||
i.e. hidden from the app UI etc.
|
||||
|
||||
Any table which is "missing" from core schema, from the custom app's
|
||||
perspective, should be added as a custom table.
|
||||
|
||||
Also, any table which is "present but missing columns" from the app's
|
||||
perspective, will require a custom table. In this case each record in the
|
||||
custom table will "tie back to" the core table record. The custom record will
|
||||
then supply any additional data for the core record.
|
||||
|
||||
Defining custom tables, and associated tasks, are documented in
|
||||
:doc:`../schemachange`.
|
52
docs/conf.py
52
docs/conf.py
|
@ -1,52 +0,0 @@
|
|||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
from importlib.metadata import version as get_version
|
||||
|
||||
project = 'Tailbone'
|
||||
copyright = '2010 - 2024, Lance Edgar'
|
||||
author = 'Lance Edgar'
|
||||
release = get_version('Tailbone')
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.viewcode',
|
||||
]
|
||||
|
||||
templates_path = ['_templates']
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
intersphinx_mapping = {
|
||||
'rattail': ('https://docs.wuttaproject.org/rattail/', None),
|
||||
'webhelpers2': ('https://webhelpers2.readthedocs.io/en/latest/', None),
|
||||
'wuttaweb': ('https://docs.wuttaproject.org/wuttaweb/', None),
|
||||
'wuttjamaican': ('https://docs.wuttaproject.org/wuttjamaican/', None),
|
||||
}
|
||||
|
||||
# allow todo entries to show up
|
||||
todo_include_todos = True
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
html_theme = 'furo'
|
||||
html_static_path = ['_static']
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
#html_logo = 'images/rattail_avatar.png'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
#htmlhelp_basename = 'Tailbonedoc'
|
|
@ -1,78 +0,0 @@
|
|||
|
||||
Development Environment
|
||||
=======================
|
||||
|
||||
.. contents:: :local:
|
||||
|
||||
Base System
|
||||
-----------
|
||||
|
||||
Development for Tailbone in particular is assumed to occur on a Linux machine.
|
||||
This is because it's assumed that the web app would run on Linux. It should be
|
||||
possible (presumably) to do either on Windows or Mac but that is not officially
|
||||
supported.
|
||||
|
||||
Furthermore it is assumed the Linux flavor in use is either Debian or Ubuntu,
|
||||
or a similar alternative. Presumably any Linux would work although some
|
||||
details may differ from what's shown here.
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
|
||||
Python
|
||||
^^^^^^
|
||||
|
||||
The only supported Python is 2.7. Of course that should already be present on
|
||||
Linux.
|
||||
|
||||
It usually is required at some point to compile C code for certain Python
|
||||
extension modules. In practice this means you probably want the Python header
|
||||
files as well:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
sudo apt-get install python-dev
|
||||
|
||||
pip
|
||||
^^^
|
||||
|
||||
The only supported Python package manager is ``pip``. This can be installed a
|
||||
few ways, one of which is:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
sudo apt-get install python-pip
|
||||
|
||||
virtualenvwrapper
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
While not technically required, it is recommended to use ``virtualenvwrapper``
|
||||
as well. There is more than one way to set this up, e.g.:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
sudo apt-get install python-virtualenvwrapper
|
||||
|
||||
The main variable as concerns these docs, is where your virtual environment(s)
|
||||
will live. If you install virtualenvwrapper via the above command, then most
|
||||
likely your ``$WORKON_HOME`` environment variable will be set to
|
||||
``~/.virtualenvs`` - however these docs will assume ``/srv/envs`` instead.
|
||||
Please adjust any commands as needed.
|
||||
|
||||
PostgreSQL
|
||||
^^^^^^^^^^
|
||||
|
||||
The other primary requirement is PostgreSQL. Technically that may be installed
|
||||
on a separate machine, which allows connection from the development machine.
|
||||
But of course it will usually just be installed on the dev machine:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
sudo apt-get install postgresql
|
||||
|
||||
Regardless of where your PG server lives, you will probably need some extras in
|
||||
order to compile extensions for the ``psycopg2`` package:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
sudo apt-get install libpq-dev
|
Binary file not shown.
Before Width: | Height: | Size: 22 KiB |
Binary file not shown.
Before Width: | Height: | Size: 7.6 KiB |
|
@ -1,85 +0,0 @@
|
|||
|
||||
Tailbone
|
||||
========
|
||||
|
||||
Welcome to Tailbone, part of the Rattail project. While the core Rattail
|
||||
package provides the data layer, the Tailbone package provides the (default,
|
||||
back-end) web application layer.
|
||||
|
||||
Some additional information is available on the `website`_. Certainly not
|
||||
everything is documented yet, but here you can see what has received some
|
||||
attention thus far.
|
||||
|
||||
.. _website: https://rattailproject.org/
|
||||
|
||||
Quick Start for Custom Apps:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
structure
|
||||
devenv
|
||||
newproject
|
||||
schemachange
|
||||
|
||||
Concept Guide:
|
||||
|
||||
.. toctree::
|
||||
|
||||
concepts/config
|
||||
concepts/console
|
||||
concepts/schema
|
||||
concepts/batches
|
||||
|
||||
Narrative Documentation:
|
||||
|
||||
.. toctree::
|
||||
|
||||
narr/batches
|
||||
|
||||
Package API:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
api/api/batch/core
|
||||
api/api/batch/ordering
|
||||
api/db
|
||||
api/diffs
|
||||
api/forms
|
||||
api/forms.widgets
|
||||
api/grids
|
||||
api/grids.core
|
||||
api/progress
|
||||
api/subscribers
|
||||
api/util
|
||||
api/views/batch
|
||||
api/views/batch.vendorcatalog
|
||||
api/views/core
|
||||
api/views/master
|
||||
api/views/members
|
||||
api/views/purchasing.batch
|
||||
api/views/purchasing.ordering
|
||||
|
||||
|
||||
Changelog:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
changelog
|
||||
|
||||
|
||||
Documentation To-Do
|
||||
===================
|
||||
|
||||
.. todolist::
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
242
docs/make.bat
242
docs/make.bat
|
@ -1,242 +0,0 @@
|
|||
@ECHO OFF
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set BUILDDIR=_build
|
||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
||||
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
||||
if NOT "%PAPER%" == "" (
|
||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
if "%1" == "help" (
|
||||
:help
|
||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||
echo. html to make standalone HTML files
|
||||
echo. dirhtml to make HTML files named index.html in directories
|
||||
echo. singlehtml to make a single large HTML file
|
||||
echo. pickle to make pickle files
|
||||
echo. json to make JSON files
|
||||
echo. htmlhelp to make HTML files and a HTML help project
|
||||
echo. qthelp to make HTML files and a qthelp project
|
||||
echo. devhelp to make HTML files and a Devhelp project
|
||||
echo. epub to make an epub
|
||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||
echo. text to make text files
|
||||
echo. man to make manual pages
|
||||
echo. texinfo to make Texinfo files
|
||||
echo. gettext to make PO message catalogs
|
||||
echo. changes to make an overview over all changed/added/deprecated items
|
||||
echo. xml to make Docutils-native XML files
|
||||
echo. pseudoxml to make pseudoxml-XML files for display purposes
|
||||
echo. linkcheck to check all external links for integrity
|
||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "clean" (
|
||||
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||
del /q /s %BUILDDIR%\*
|
||||
goto end
|
||||
)
|
||||
|
||||
|
||||
%SPHINXBUILD% 2> nul
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if "%1" == "html" (
|
||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "dirhtml" (
|
||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "singlehtml" (
|
||||
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pickle" (
|
||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the pickle files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "json" (
|
||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the JSON files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "htmlhelp" (
|
||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "qthelp" (
|
||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Tailbone.qhcp
|
||||
echo.To view the help file:
|
||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Tailbone.ghc
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "devhelp" (
|
||||
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "epub" (
|
||||
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latex" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latexpdf" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
cd %BUILDDIR%/latex
|
||||
make all-pdf
|
||||
cd %BUILDDIR%/..
|
||||
echo.
|
||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latexpdfja" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
cd %BUILDDIR%/latex
|
||||
make all-pdf-ja
|
||||
cd %BUILDDIR%/..
|
||||
echo.
|
||||
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "text" (
|
||||
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "man" (
|
||||
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "texinfo" (
|
||||
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "gettext" (
|
||||
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "changes" (
|
||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.The overview file is in %BUILDDIR%/changes.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "linkcheck" (
|
||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Link check complete; look for any errors in the above output ^
|
||||
or in %BUILDDIR%/linkcheck/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "doctest" (
|
||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of doctests in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/doctest/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "xml" (
|
||||
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The XML files are in %BUILDDIR%/xml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pseudoxml" (
|
||||
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
|
||||
goto end
|
||||
)
|
||||
|
||||
:end
|
|
@ -1,74 +0,0 @@
|
|||
.. -*- coding: utf-8 -*-
|
||||
|
||||
Data Batches
|
||||
============
|
||||
|
||||
This document briefly outlines what comprises a batch in terms of the Tailbone
|
||||
user interface etc.
|
||||
|
||||
|
||||
Batch Views
|
||||
-----------
|
||||
|
||||
Adding support for a new batch type is mostly a matter of providing some custom
|
||||
views for the batch and its rows. In fact you must define four different view
|
||||
classes, inheriting from each of the following:
|
||||
|
||||
* :class:`tailbone.views.batch.BatchGrid`
|
||||
* :class:`tailbone.views.batch.BatchCrud`
|
||||
* :class:`tailbone.views.batch.BatchRowGrid`
|
||||
* :class:`tailbone.views.batch.BatchRowCrud`
|
||||
|
||||
It would sure be nice to only require two view classes instead of four, hopefully
|
||||
that can happen "soon". In the meantime that's what it takes. Note that as with
|
||||
batch data models, there are some more specialized parent classes which you may
|
||||
want to inherit from instead of the core classes mentioned above:
|
||||
|
||||
* :class:`tailbone.views.batch.FileBatchGrid`
|
||||
* :class:`tailbone.views.batch.FileBatchCrud`
|
||||
* :class:`tailbone.views.batch.ProductBatchRowGrid`
|
||||
|
||||
Here are the vendor catalog views as examples:
|
||||
|
||||
* :class:`tailbone.views.vendors.catalogs.VendorCatalogGrid`
|
||||
* :class:`tailbone.views.vendors.catalogs.VendorCatalogCrud`
|
||||
* :class:`tailbone.views.vendors.catalogs.VendorCatalogRowGrid`
|
||||
* :class:`tailbone.views.vendors.catalogs.VendorCatalogRowCrud`
|
||||
|
||||
|
||||
Pyramid Config
|
||||
--------------
|
||||
|
||||
In addition to defining the batch views, the Pyramid Configurator object must be
|
||||
told of the views and their routes. This also could probably stand to be simpler
|
||||
somehow, but for now the easiest thing is to apply default configuration with:
|
||||
|
||||
* :func:`tailbone.views.batch.defaults()`
|
||||
|
||||
See the source behind the vendor catalog for an example:
|
||||
|
||||
* :func:`tailbone.views.vendors.catalogs.includeme()`
|
||||
|
||||
Note of course that your view config must be included by the core/upstream
|
||||
config process of your application's startup to take effect. At this point
|
||||
your views should be accessible by navigating to the URLs directly, e.g. for
|
||||
the vendor catalog views:
|
||||
|
||||
* List Uploaded Catalogs - http://example.com/vendors/catalogs/
|
||||
* Upload New Catlaog - http://example.com/vendors/catalogs/new
|
||||
|
||||
|
||||
Menu and Templates
|
||||
------------------
|
||||
|
||||
Providing access to the batch views is (I think) the last step. You must add
|
||||
links to the views, wherever that makes sense for your app. In case it's
|
||||
helpful, here's a Mako template snippet which would show some links to the main
|
||||
vendor catalog views:
|
||||
|
||||
.. code-block:: mako
|
||||
|
||||
<ul>
|
||||
<li>${h.link_to("Vendor Catalogs", url('vendors.catalogs'))}</li>
|
||||
<li>${h.link_to("Upload new Vendor Catalog", url('vendors.catalogs.create'))}</li>
|
||||
</ul>
|
|
@ -1,154 +0,0 @@
|
|||
|
||||
Creating a New Project
|
||||
======================
|
||||
|
||||
.. contents:: :local:
|
||||
|
||||
.. highlight:: bash
|
||||
|
||||
This describes the process of creating a new app project based on
|
||||
Rattail/Tailbone. It assumes you are working from a supported :doc:`devenv`.
|
||||
|
||||
Per convention, this doc uses "Poser" (and ``poser``) to represent the custom
|
||||
app. Please adjust commands etc. accordingly. See also :doc:`structure`.
|
||||
|
||||
|
||||
Create the Virtual Environment
|
||||
------------------------------
|
||||
|
||||
First step is simple enough::
|
||||
|
||||
mkvirtualenv poser
|
||||
|
||||
Then with your new environment activated, install the Tailbone package::
|
||||
|
||||
pip install Tailbone
|
||||
|
||||
|
||||
Create the Project
|
||||
------------------
|
||||
|
||||
Now with your environment still activated, ``cd`` to wherever you like
|
||||
(e.g. ``~/src``) and create a new project skeleton like so::
|
||||
|
||||
mkdir -p ~/src
|
||||
cd ~/src
|
||||
pcreate -s rattail poser
|
||||
|
||||
This will have created a new project at ``~/src/poser`` which you can then edit
|
||||
as you wish. At some point you will need to "install" this project to the
|
||||
environment like so (again with environment active)::
|
||||
|
||||
cd ~/src/poser
|
||||
pip install -e .
|
||||
|
||||
|
||||
Setup the App Environment
|
||||
-------------------------
|
||||
|
||||
Any project based on Rattail will effectively be its own "app" (usually), but
|
||||
Rattail itself provides some app functionality as well. However all such apps
|
||||
require config files, usually. If running a web app then you may also need to
|
||||
have configured a folder for session storage, etc. To hopefully simplify all
|
||||
this, there are a few commands you should now run, with your virtual
|
||||
environment still active::
|
||||
|
||||
rattail make-appdir
|
||||
cdvirtualenv app
|
||||
rattail make-config -T rattail
|
||||
rattail make-config -T quiet
|
||||
rattail make-config -T web
|
||||
|
||||
This will have created a new 'app' folder in your environment (e.g. at
|
||||
``/srv/envs/poser/app``) and then created ``rattail.conf`` and ``web.conf``
|
||||
files within that app dir. Note that there will be other folders inside the
|
||||
app dir as well; these are referenced by the config files.
|
||||
|
||||
But you're not done yet... You should likely edit the config files, at the
|
||||
very least edit ``rattail.conf`` and change the ``default.url`` value (under
|
||||
``[rattail.db]`` section) which defines the Rattail database connection.
|
||||
|
||||
|
||||
Create the Database
|
||||
-------------------
|
||||
|
||||
If applicable, it's time for that. First you must literally create the user
|
||||
and database on your PostgreSQL server, e.g.::
|
||||
|
||||
sudo -u postgres createuser --no-createdb --no-createrole --no-superuser poser
|
||||
sudo -u postgres psql -c "alter user poser password 'mypassword'"
|
||||
sudo -u postgres createdb --owner poser poser
|
||||
|
||||
Then you can install the schema; with your virtual environment activated::
|
||||
|
||||
cdvirtualenv
|
||||
alembic -c app/rattail.conf upgrade heads
|
||||
|
||||
At this point your 'poser' database should have some empty tables. To confirm,
|
||||
on your PG server do::
|
||||
|
||||
sudo -u postgres psql -c '\d' poser
|
||||
|
||||
|
||||
Create Admin User
|
||||
-----------------
|
||||
|
||||
If your intention is to have a web app, or at least to test one, you'll
|
||||
probably want to create the initial admin user. With your env active::
|
||||
|
||||
cdvirtualenv
|
||||
rattail -c app/quiet.conf make-user --admin myusername
|
||||
|
||||
This should prompt you for a password, then create a single user and assign it
|
||||
to the Administrator role.
|
||||
|
||||
|
||||
Install Sample Data
|
||||
-------------------
|
||||
|
||||
If desired, you can install a bit of sample data to your fresh Rattail
|
||||
database. With your env active do::
|
||||
|
||||
cdvirtualenv
|
||||
rattail -c app/quiet.conf -P import-sample
|
||||
|
||||
|
||||
Run Dev Web Server
|
||||
------------------
|
||||
|
||||
With all the above in place, you may now run the web server in dev mode::
|
||||
|
||||
cdvirtualenv
|
||||
pserve --reload app/web.conf
|
||||
|
||||
And finally..you may browse your new project dev site at http://localhost:9080/
|
||||
(unless you changed the port etc.)
|
||||
|
||||
|
||||
Schema Migrations
|
||||
-----------------
|
||||
|
||||
Often a new project will require custom schema additions to track/manage data
|
||||
unique to the project. Rattail uses `Alembic`_ for handling schema migrations.
|
||||
General usage of that is documented elsewhere, but a little should be said here
|
||||
regarding new projects.
|
||||
|
||||
.. _Alembic: https://pypi.python.org/pypi/alembic
|
||||
|
||||
The new project template includes most of an Alembic "repo" for schema
|
||||
migrations. However there is one step required to really bootstrap it, i.e. to
|
||||
the point where normal Alembic usage will work: you must create the initial
|
||||
version script. Before you do this, you should be reasonably happy with any
|
||||
ORM classes you've defined, as the initial version script will be used to
|
||||
create that schema. Once you're ready for the script, this command should do
|
||||
it::
|
||||
|
||||
cdvirtualenv
|
||||
bin/alembic -c app/rattail.conf revision --autogenerate --version-path ~/src/poser/poser/db/alembic/versions/ -m 'initial Poser tables'
|
||||
|
||||
You should of course look over and edit the generated script as needed. One
|
||||
change in particular you should make is to add a branch label, e.g.:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
branch_labels = ('poser',)
|
|
@ -1,63 +0,0 @@
|
|||
|
||||
Migrating the Schema
|
||||
====================
|
||||
|
||||
.. contents:: :local:
|
||||
|
||||
As development progresses for your custom app, you may need to migrate the
|
||||
database schema from time to time.
|
||||
|
||||
See also this general discussion of the :doc:`concepts/schema`.
|
||||
|
||||
.. note::
|
||||
|
||||
The only "safe" migrations are those which add or modify (or remove)
|
||||
"custom" tables, i.e. those *not* provided by the ``rattail.db.model``
|
||||
package. This doc assumes you are aware of this and are only attempting a
|
||||
safe migration.
|
||||
|
||||
|
||||
Modify ORM Classes
|
||||
------------------
|
||||
|
||||
First step is to modify the ORM classes defined by your app, so they reflect
|
||||
the "desired" schema. Typically this will mean editing files under the
|
||||
``poser.db.model`` package within your source. In particular when adding new
|
||||
tables, you must be sure to include them within ``poser/db/model/__init__.py``.
|
||||
|
||||
As noted above, only those classes *not* provided by ``rattail.db.model``
|
||||
should be modified here, to be safe. If you wish to "extend" an existing
|
||||
table, you must create a secondary table which ties back to the first via
|
||||
one-to-one foreign key relationship.
|
||||
|
||||
|
||||
Create Migration Script
|
||||
-----------------------
|
||||
|
||||
Next you will create the Alembic script which is responsible for performing the
|
||||
schema migration against a database. This is typically done like so:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
workon poser
|
||||
cdvirtualenv
|
||||
bin/alembic -c app/rattail.conf revision --autogenerate --head poser@head -m "describe migration here"
|
||||
|
||||
This will create a new file under
|
||||
e.g. ``~/src/poser/poser/db/alembic/versions/``. You should edit this file as
|
||||
needed to ensure it performs all steps required for the migration. Technically
|
||||
it should support downgrade as well as upgrade, although in practice that isn't
|
||||
always required.
|
||||
|
||||
|
||||
Upgrade Database Schema
|
||||
-----------------------
|
||||
|
||||
Once you're happy with the new script, you can apply it against your dev
|
||||
database with something like:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
workon poser
|
||||
cdvirtualenv
|
||||
bin/alembic -c app/rattail.conf upgrade heads
|
|
@ -1,130 +0,0 @@
|
|||
|
||||
App Organization & Structure
|
||||
============================
|
||||
|
||||
.. contents:: :local:
|
||||
|
||||
Tailbone doesn't try to be an "app" proper. But it does try to provide just
|
||||
about everything you'd need to make one. These docs assume you are making a
|
||||
custom app, and will refer to the app as "Poser" to be consistent. In practice
|
||||
you would give your app a unique name which is meaningful to you. Please
|
||||
mentally replace "Poser" with your app name as you read.
|
||||
|
||||
.. note::
|
||||
|
||||
Technically it *is possible* to use Tailbone directly as the app. You may
|
||||
do so for basic testing of the concepts, but you'd be stuck with Tailbone
|
||||
logic, with far fewer customization options. All docs will assume a custom
|
||||
"Poser" app which wraps and (as necessary) overrides Tailbone and Rattail.
|
||||
|
||||
|
||||
Architecture
|
||||
------------
|
||||
|
||||
In terms of how the Poser app hangs together, here is a conceptual diagram.
|
||||
Note that all systems on the right-hand side are *external* to Poser, i.e. they
|
||||
are not "plugins" although Poser may use plugin-like logic for the sake of
|
||||
integrating with these systems.
|
||||
|
||||
.. image:: images/poser-architecture.png
|
||||
|
||||
|
||||
Data Layer vs. Web Layer
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
While the above graphic doesn't do a great job highlighting the difference, it
|
||||
will (presumably) help to understand the difference in purpose and function of
|
||||
Tailbone vs. Rattail packages.
|
||||
|
||||
**Rattail** is the data layer, and is responsible for database connectivity,
|
||||
table schema information, and some business rules logic (among other things).
|
||||
|
||||
**Tailbone** is the web app layer, and is responsible for presentation and
|
||||
management of data objects which are made available by Rattail (and others).
|
||||
|
||||
**Poser** is a custom layer which can make use of both data and web app layers,
|
||||
supplementing each as necessary. In practice the lines may get blurry within
|
||||
Poser.
|
||||
|
||||
The reason for this distinction between layers, is to allow creation of custom
|
||||
apps which use only the data layer but not the web app layer. This can be
|
||||
useful for console-based apps; a traditional GUI app would also be possible
|
||||
although none is yet planned.
|
||||
|
||||
|
||||
File Layout
|
||||
-----------
|
||||
|
||||
Below is an example file layout for a Poser app project. This tries to be
|
||||
"complete" and show most kinds of files a typical project may need. In
|
||||
practice you can usually ignore anything which doesn't apply to your app,
|
||||
i.e. relatively few of the files shown here are actually required. Of course
|
||||
some apps may need many more files than this to achieve their goals.
|
||||
|
||||
Note that all files in the root ``poser`` package namespace would correspond to
|
||||
the "data layer" mentioned above, whereas everything under ``poser.web`` would
|
||||
of course supply the web app layer.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
~/src/poser/
|
||||
├── CHANGELOG.md
|
||||
├── docs/
|
||||
├── fabfile.py
|
||||
├── MANIFEST.in
|
||||
├── poser/
|
||||
│ ├── __init__.py
|
||||
│ ├── batch/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── foobatch.py
|
||||
│ ├── commands.py
|
||||
│ ├── config.py
|
||||
│ ├── datasync/
|
||||
│ ├── db/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── alembic/
|
||||
│ │ └── model/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── batch/
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ └── foobatch.py
|
||||
│ │ └── customers.py
|
||||
│ ├── emails.py
|
||||
│ ├── enum.py
|
||||
│ ├── importing/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── model.py
|
||||
│ │ ├── poser.py
|
||||
│ │ └── versions.py
|
||||
│ ├── problems.py
|
||||
│ ├── templates/
|
||||
│ │ └── mail/
|
||||
│ │ └── warn_about_foo.html.mako
|
||||
│ ├── _version.py
|
||||
│ └── web/
|
||||
│ ├── __init__.py
|
||||
│ ├── app.py
|
||||
│ ├── static/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── css/
|
||||
│ │ ├── favicon.ico
|
||||
│ │ ├── img/
|
||||
│ │ └── js/
|
||||
│ ├── subscribers.py
|
||||
│ ├── templates/
|
||||
│ │ ├── base.mako
|
||||
│ │ ├── batch/
|
||||
│ │ │ └── foobatch/
|
||||
│ │ ├── customers/
|
||||
│ │ ├── menu.mako
|
||||
│ │ └── products/
|
||||
│ └── views/
|
||||
│ ├── __init__.py
|
||||
│ ├── batch/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── foobatch.py
|
||||
│ ├── common.py
|
||||
│ ├── customers.py
|
||||
│ └── products.py
|
||||
├── README.rst
|
||||
└── setup.py
|
103
pyproject.toml
103
pyproject.toml
|
@ -1,103 +0,0 @@
|
|||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
|
||||
[project]
|
||||
name = "Tailbone"
|
||||
version = "0.22.8"
|
||||
description = "Backoffice Web Application for Rattail"
|
||||
readme = "README.md"
|
||||
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
||||
license = {text = "GNU GPL v3+"}
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Web Environment",
|
||||
"Framework :: Pyramid",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
||||
"Natural Language :: English",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
"Topic :: Office/Business",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
]
|
||||
requires-python = ">= 3.8"
|
||||
dependencies = [
|
||||
"asgiref",
|
||||
"colander",
|
||||
"ColanderAlchemy",
|
||||
"cornice",
|
||||
"cornice-swagger",
|
||||
"deform",
|
||||
"humanize",
|
||||
"Mako",
|
||||
"markdown",
|
||||
"openpyxl",
|
||||
"paginate",
|
||||
"paginate_sqlalchemy",
|
||||
"passlib",
|
||||
"Pillow",
|
||||
"pyramid>=2",
|
||||
"pyramid_beaker",
|
||||
"pyramid_deform",
|
||||
"pyramid_exclog",
|
||||
"pyramid_fanstatic",
|
||||
"pyramid_mako",
|
||||
"pyramid_retry",
|
||||
"pyramid_tm",
|
||||
"rattail[db,bouncer]>=0.20.1",
|
||||
"sa-filters",
|
||||
"simplejson",
|
||||
"transaction",
|
||||
"waitress",
|
||||
"WebHelpers2",
|
||||
"WuttaWeb>=0.21.0",
|
||||
"zope.sqlalchemy>=1.5",
|
||||
]
|
||||
|
||||
|
||||
[project.optional-dependencies]
|
||||
docs = ["Sphinx", "furo"]
|
||||
tests = ["coverage", "mock", "pytest", "pytest-cov"]
|
||||
|
||||
|
||||
[project.entry-points."paste.app_factory"]
|
||||
main = "tailbone.app:main"
|
||||
webapi = "tailbone.webapi:main"
|
||||
|
||||
|
||||
[project.entry-points."rattail.cleaners"]
|
||||
beaker = "tailbone.cleanup:BeakerCleaner"
|
||||
|
||||
|
||||
[project.entry-points."rattail.config.extensions"]
|
||||
tailbone = "tailbone.config:ConfigExtension"
|
||||
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://rattailproject.org"
|
||||
Repository = "https://forgejo.wuttaproject.org/rattail/tailbone"
|
||||
Issues = "https://forgejo.wuttaproject.org/rattail/tailbone/issues"
|
||||
Changelog = "https://forgejo.wuttaproject.org/rattail/tailbone/src/branch/master/CHANGELOG.md"
|
||||
|
||||
|
||||
[tool.commitizen]
|
||||
version_provider = "pep621"
|
||||
tag_format = "v$version"
|
||||
update_changelog_on_bump = true
|
||||
|
||||
|
||||
[tool.nosetests]
|
||||
nocapture = 1
|
||||
cover-package = "tailbone"
|
||||
cover-erase = 1
|
||||
cover-html = 1
|
||||
cover-html-dir = "htmlcov"
|
1
rattail/__init__.py
Normal file
1
rattail/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
__import__('pkg_resources').declare_namespace(__name__)
|
34
rattail/pyramid/__init__.py
Normal file
34
rattail/pyramid/__init__.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.pyramid`` -- Rattail's Pyramid Framework
|
||||
"""
|
||||
|
||||
from rattail.pyramid._version import __version__
|
||||
|
||||
|
||||
def includeme(config):
|
||||
config.include('rattail.pyramid.subscribers')
|
||||
config.include('rattail.pyramid.views')
|
1
rattail/pyramid/_version.py
Normal file
1
rattail/pyramid/_version.py
Normal file
|
@ -0,0 +1 @@
|
|||
__version__ = '0.3a15'
|
85
rattail/pyramid/forms.py
Normal file
85
rattail/pyramid/forms.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.pyramid.forms`` -- Rattail Forms
|
||||
"""
|
||||
|
||||
from webhelpers.html import literal
|
||||
|
||||
import formalchemy
|
||||
|
||||
from edbob.pyramid.forms import pretty_datetime
|
||||
|
||||
import rattail
|
||||
from rattail.gpc import GPC
|
||||
|
||||
|
||||
class GPCFieldRenderer(formalchemy.TextFieldRenderer):
|
||||
"""
|
||||
Renderer for :class:`rattail.gpc.GPC` fields.
|
||||
"""
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
# Hm, should maybe consider hard-coding this...?
|
||||
return len(str(GPC(0)))
|
||||
|
||||
|
||||
class PriceFieldRenderer(formalchemy.TextFieldRenderer):
|
||||
"""
|
||||
Renderer for fields which reference a :class:`ProductPrice` instance.
|
||||
"""
|
||||
|
||||
def render_readonly(self, **kwargs):
|
||||
price = self.field.raw_value
|
||||
if price:
|
||||
if price.price is not None and price.pack_price is not None:
|
||||
if price.multiple > 1:
|
||||
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)
|
||||
return '$ %0.2f' % price.price
|
||||
if price.pack_price is not None:
|
||||
return '$ %0.2f / %u' % (price.pack_price, price.pack_multiple)
|
||||
return ''
|
||||
|
||||
|
||||
class PriceWithExpirationFieldRenderer(PriceFieldRenderer):
|
||||
"""
|
||||
Price field renderer which also displays the expiration date, if present.
|
||||
"""
|
||||
|
||||
def render_readonly(self, **kwargs):
|
||||
res = super(PriceWithExpirationFieldRenderer, self).render_readonly(**kwargs)
|
||||
if res:
|
||||
price = self.field.raw_value
|
||||
if price.ends:
|
||||
res += ' (%s)' % pretty_datetime(price.ends, from_='utc')
|
||||
return res
|
|
@ -1,4 +1,3 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html style="direction: ltr;" xmlns="http://www.w3.org/1999/xhtml" lang="en-us">
|
||||
<head>
|
||||
|
@ -6,10 +5,6 @@
|
|||
<title>Ordering Worksheet : ${vendor.name}</title>
|
||||
<style type="text/css">
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
margin: 10px 0px;
|
||||
|
@ -62,11 +57,6 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
td.code {
|
||||
font-family: monospace;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
td.scratch_pad {
|
||||
width: 20px;
|
||||
}
|
||||
|
@ -89,29 +79,25 @@
|
|||
<table>
|
||||
% for dept in sorted(costs, key=lambda x: x.name):
|
||||
<tr>
|
||||
<th class="department" colspan="21">Department: ${dept.name} (${dept.number})</th>
|
||||
<th class="department" colspan="19">Department: ${dept.name} (${dept.number})</th>
|
||||
</tr>
|
||||
% for subdept in sorted(costs[dept], key=lambda x: x.name):
|
||||
<tr>
|
||||
<th class="subdepartment" colspan="21">Subdepartment: ${subdept.name} (${subdept.number})</th>
|
||||
<th class="subdepartment" colspan="19">Subdepartment: ${subdept.name} (${subdept.number})</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>UPC</th>
|
||||
<th>Brand</th>
|
||||
<th>Description</th>
|
||||
<th>Size</th>
|
||||
<th>Case</th>
|
||||
<th>Case Qty.</th>
|
||||
<th>Vend. Code</th>
|
||||
<th>Pref.</th>
|
||||
<th>Preferred</th>
|
||||
<th colspan="14">Order Scratch Pad</th>
|
||||
</tr>
|
||||
% for cost in sorted(costs[dept][subdept], key=cost_sort_key):
|
||||
% for cost in sorted(costs[dept][subdept], key=lambda x: x.product.description):
|
||||
<tr>
|
||||
<td class="upc">${get_upc(cost.product)}</td>
|
||||
<td class="brand">${cost.product.brand or ''}</td>
|
||||
<td class="desc">${cost.product.description}</td>
|
||||
<td class="size">${cost.product.size or ''}</td>
|
||||
<td class="case-qty">${app.render_quantity(cost.case_size)} ${"LB" if cost.product.weighed else "EA"}</td>
|
||||
<td class="case-qty">${cost.case_size} ${rattail.UNIT_OF_MEASURE.get(cost.product.unit_of_measure, '')}</td>
|
||||
<td class="code">${cost.code or ''}</td>
|
||||
<td class="preferred">${'X' if cost.preference == 1 else ''}</td>
|
||||
% for i in range(14):
|
||||
|
@ -120,7 +106,7 @@
|
|||
</tr>
|
||||
% endfor
|
||||
<tr>
|
||||
<td class="spacer" colspan="21">
|
||||
<td class="spacer" colspan="19">
|
||||
</tr>
|
||||
% endfor
|
||||
% endfor
|
55
rattail/pyramid/subscribers.py
Normal file
55
rattail/pyramid/subscribers.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.pyramid.subscribers`` -- Event Subscribers
|
||||
"""
|
||||
|
||||
from pyramid import threadlocal
|
||||
|
||||
import rattail
|
||||
|
||||
|
||||
def before_render(event):
|
||||
"""
|
||||
Adds goodies to the global template renderer context:
|
||||
|
||||
* ``rattail``
|
||||
"""
|
||||
|
||||
# Import labels module so it's available if/when needed.
|
||||
import rattail.labels
|
||||
|
||||
# Import SIL module so it's available if/when needed.
|
||||
import rattail.sil
|
||||
|
||||
request = event.get('request') or threadlocal.get_current_request()
|
||||
|
||||
renderer_globals = event
|
||||
renderer_globals['rattail'] = rattail
|
||||
|
||||
|
||||
def includeme(config):
|
||||
config.add_subscriber('rattail.pyramid.subscribers:before_render',
|
||||
'pyramid.events.BeforeRender')
|
11
rattail/pyramid/templates/batches/crud.mako
Normal file
11
rattail/pyramid/templates/batches/crud.mako
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Batches", url('batches'))}</li>
|
||||
<li>${h.link_to("View Batch Rows", url('batch.rows', uuid=form.fieldset.model.uuid))}</li>
|
||||
% if not form.readonly:
|
||||
<li>${h.link_to("View this Batch", url('batch.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
5
rattail/pyramid/templates/batches/index.mako
Normal file
5
rattail/pyramid/templates/batches/index.mako
Normal file
|
@ -0,0 +1,5 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Batches</%def>
|
||||
|
||||
${parent.body()}
|
42
rattail/pyramid/templates/batches/params.mako
Normal file
42
rattail/pyramid/templates/batches/params.mako
Normal file
|
@ -0,0 +1,42 @@
|
|||
<%inherit file="/base.mako" />
|
||||
|
||||
<%def name="title()">Batch Parameters</%def>
|
||||
|
||||
<%def name="head_tags()">
|
||||
${parent.head_tags()}
|
||||
<script language="javascript" type="text/javascript">
|
||||
|
||||
$(function() {
|
||||
|
||||
$('#create-batch').click(function() {
|
||||
disable_button(this, "Creating batch");
|
||||
disable_button('#cancel');
|
||||
$('form').submit();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
<%def name="batch_params()"></%def>
|
||||
|
||||
<p>Please provide the following values for your new batch:</p>
|
||||
<br />
|
||||
|
||||
<div class="form">
|
||||
|
||||
${h.form(request.get_referrer())}
|
||||
${h.hidden('provider', value=provider)}
|
||||
${h.hidden('params', value='True')}
|
||||
|
||||
${self.batch_params()}
|
||||
|
||||
<div class="buttons">
|
||||
<button type="button" id="create-batch">Create Batch</button>
|
||||
<button type="button" id="cancel" onclick="location.href = '${request.get_referrer()}';">Cancel</button>
|
||||
</div>
|
||||
|
||||
${h.end_form()}
|
||||
|
||||
</div>
|
19
rattail/pyramid/templates/batches/params/print_labels.mako
Normal file
19
rattail/pyramid/templates/batches/params/print_labels.mako
Normal file
|
@ -0,0 +1,19 @@
|
|||
<%inherit file="/batches/params.mako" />
|
||||
|
||||
<%def name="batch_params()">
|
||||
|
||||
<div class="field-wrapper">
|
||||
<label for="profile">Label Type</label>
|
||||
<div class="field">
|
||||
${h.select('profile', None, label_profiles)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-wrapper">
|
||||
<label for="quantity">Quantity</label>
|
||||
<div class="field">${h.text('quantity', value=1)}</div>
|
||||
</div>
|
||||
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
40
rattail/pyramid/templates/batches/read.mako
Normal file
40
rattail/pyramid/templates/batches/read.mako
Normal file
|
@ -0,0 +1,40 @@
|
|||
<%inherit file="/batches/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
${parent.context_menu_items()}
|
||||
<li>${h.link_to("Edit this Batch", url('batch.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
<li>${h.link_to("Delete this Batch", url('batch.delete', uuid=form.fieldset.model.uuid))}</li>
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
||||
|
||||
<% batch = form.fieldset.model %>
|
||||
|
||||
<h2>Columns</h2>
|
||||
|
||||
<div class="grid full hoverable">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>SIL Name</th>
|
||||
<th>Display Name</th>
|
||||
<th>Description</th>
|
||||
<th>Data Type</th>
|
||||
<th>Visible</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for i, column in enumerate(batch.columns, 1):
|
||||
<tr class="${'odd' if i % 2 else 'even'}">
|
||||
<td>${column.name}</td>
|
||||
<td>${column.sil_name}</td>
|
||||
<td>${column.display_name}</td>
|
||||
<td>${column.description}</td>
|
||||
<td>${column.data_type}</td>
|
||||
<td>${column.visible}</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
8
rattail/pyramid/templates/batches/rows/crud.mako
Normal file
8
rattail/pyramid/templates/batches/rows/crud.mako
Normal file
|
@ -0,0 +1,8 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Batch", url('batch.read', uuid=form.fieldset.model.batch.uuid))}</li>
|
||||
<li>${h.link_to("Back to Batch Rows", url('batch.rows', uuid=form.fieldset.model.batch.uuid))}</li>
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
46
rattail/pyramid/templates/batches/rows/index.mako
Normal file
46
rattail/pyramid/templates/batches/rows/index.mako
Normal file
|
@ -0,0 +1,46 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Batch Rows : ${batch.description}</%def>
|
||||
|
||||
<%def name="head_tags()">
|
||||
${parent.head_tags()}
|
||||
<script language="javascript" type="text/javascript">
|
||||
|
||||
$(function() {
|
||||
|
||||
$('#delete-results').click(function() {
|
||||
var msg = "This will delete all rows matching the current search.\n\n"
|
||||
+ "PLEASE NOTE that this may include some rows which are not visible "
|
||||
+ "on your screen.\n(I.e., if there is more than one \"page\" of results.)\n\n"
|
||||
+ "Are you sure you wish to delete these rows?";
|
||||
if (confirm(msg)) {
|
||||
disable_button(this, "Deleting rows");
|
||||
location.href = '${url('batch.rows.delete', uuid=batch.uuid)}';
|
||||
}
|
||||
});
|
||||
|
||||
$('#execute-batch').click(function() {
|
||||
if (confirm("Are you sure you wish to execute this batch?")) {
|
||||
disable_button(this, "Executing batch");
|
||||
location.href = '${url('batch.execute', uuid=batch.uuid)}';
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Batches", url('batches'))}</li>
|
||||
<li>${h.link_to("Back to Batch", url('batch.read', uuid=batch.uuid))}</li>
|
||||
</%def>
|
||||
|
||||
<%def name="tools()">
|
||||
<div class="buttons">
|
||||
<button type="button" id="delete-results">Delete Results</button>
|
||||
<button type="button" id="execute-batch">Execute Batch</button>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
12
rattail/pyramid/templates/brands/crud.mako
Normal file
12
rattail/pyramid/templates/brands/crud.mako
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Brands", url('brands'))}</li>
|
||||
% if form.readonly:
|
||||
<li>${h.link_to("Edit this Brand", url('brand.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this Brand", url('brand.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
11
rattail/pyramid/templates/brands/index.mako
Normal file
11
rattail/pyramid/templates/brands/index.mako
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Brands</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('brands.create'):
|
||||
<li>${h.link_to("Create a new Brand", url('brand.create'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
12
rattail/pyramid/templates/categories/crud.mako
Normal file
12
rattail/pyramid/templates/categories/crud.mako
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Categories", url('categories'))}</li>
|
||||
% if form.readonly:
|
||||
<li>${h.link_to("Edit this Category", url('category.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this Category", url('category.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
11
rattail/pyramid/templates/categories/index.mako
Normal file
11
rattail/pyramid/templates/categories/index.mako
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Categories</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('categories.create'):
|
||||
<li>${h.link_to("Create a new Category", url('category.create'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
7
rattail/pyramid/templates/customer_groups/crud.mako
Normal file
7
rattail/pyramid/templates/customer_groups/crud.mako
Normal file
|
@ -0,0 +1,7 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Customer Groups", url('customer_groups'))}</li>
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
5
rattail/pyramid/templates/customer_groups/index.mako
Normal file
5
rattail/pyramid/templates/customer_groups/index.mako
Normal file
|
@ -0,0 +1,5 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Customer Groups</%def>
|
||||
|
||||
${parent.body()}
|
7
rattail/pyramid/templates/customers/crud.mako
Normal file
7
rattail/pyramid/templates/customers/crud.mako
Normal file
|
@ -0,0 +1,7 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<p>${h.link_to("Back to Customers", url('customers'))}</p>
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
5
rattail/pyramid/templates/customers/index.mako
Normal file
5
rattail/pyramid/templates/customers/index.mako
Normal file
|
@ -0,0 +1,5 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Customers</%def>
|
||||
|
||||
${parent.body()}
|
51
rattail/pyramid/templates/customers/read.mako
Normal file
51
rattail/pyramid/templates/customers/read.mako
Normal file
|
@ -0,0 +1,51 @@
|
|||
<%inherit file="/customers/crud.mako" />
|
||||
|
||||
${parent.body()}
|
||||
|
||||
<% customer = form.fieldset.model %>
|
||||
|
||||
<h2>People</h2>
|
||||
% if customer.people:
|
||||
<p>Customer account is associated with the following people:</p>
|
||||
<div class="grid clickable">
|
||||
<table>
|
||||
<thead>
|
||||
<th>First Name</th>
|
||||
<th>Last Name</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for i, person in enumerate(customer.people, 1):
|
||||
<tr class="${'odd' if i % 2 else 'even'}" url="${url('person.read', uuid=person.uuid)}">
|
||||
<td>${person.first_name or ''}</td>
|
||||
<td>${person.last_name or ''}</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
% else:
|
||||
<p>Customer account is not associated with any people.</p>
|
||||
% endif
|
||||
|
||||
<h2>Groups</h2>
|
||||
% if customer.groups:
|
||||
<p>Customer account belongs to the following groups:</p>
|
||||
<div class="grid clickable">
|
||||
<table>
|
||||
<thead>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for i, group in enumerate(customer.groups, 1):
|
||||
<tr class="${'odd' if i % 2 else 'even'}" url="${url('customer_group.read', uuid=group.uuid)}">
|
||||
<td>${group.id}</td>
|
||||
<td>${group.name or ''}</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
% else:
|
||||
<p>Customer account doesn't belong to any groups.</p>
|
||||
% endif
|
12
rattail/pyramid/templates/departments/crud.mako
Normal file
12
rattail/pyramid/templates/departments/crud.mako
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Departments", url('departments'))}</li>
|
||||
% if form.readonly:
|
||||
<li>${h.link_to("Edit this Department", url('department.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this Department", url('department.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
11
rattail/pyramid/templates/departments/index.mako
Normal file
11
rattail/pyramid/templates/departments/index.mako
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Departments</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('departments.create'):
|
||||
<li>${h.link_to("Create a new Department", url('department.create'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
5
rattail/pyramid/templates/employees/index.mako
Normal file
5
rattail/pyramid/templates/employees/index.mako
Normal file
|
@ -0,0 +1,5 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Employees</%def>
|
||||
|
||||
${parent.body()}
|
26
rattail/pyramid/templates/labels/profiles/crud.mako
Normal file
26
rattail/pyramid/templates/labels/profiles/crud.mako
Normal file
|
@ -0,0 +1,26 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="head_tags()">
|
||||
${parent.head_tags()}
|
||||
<style type="text/css">
|
||||
|
||||
div.form div.field-wrapper.format textarea {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Label Profiles", url('label_profiles'))}</li>
|
||||
% if form.updating:
|
||||
<% profile = form.fieldset.model %>
|
||||
<% printer = profile.get_printer() %>
|
||||
% if printer and printer.required_settings:
|
||||
<li>${h.link_to("Edit Printer Settings", url('label_profile.printer_settings', uuid=profile.uuid))}</li>
|
||||
% endif
|
||||
<li>${h.link_to("View this Label Profile", url('label_profile.read', uuid=profile.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
11
rattail/pyramid/templates/labels/profiles/index.mako
Normal file
11
rattail/pyramid/templates/labels/profiles/index.mako
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Label Profiles</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('label_profiles.create'):
|
||||
<li>${h.link_to("Create a new Label Profile", url('label_profile.create'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
48
rattail/pyramid/templates/labels/profiles/printer.mako
Normal file
48
rattail/pyramid/templates/labels/profiles/printer.mako
Normal file
|
@ -0,0 +1,48 @@
|
|||
<%inherit file="/base.mako" />
|
||||
|
||||
<%def name="title()">Printer Settings</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Label Profiles", url('label_profiles'))}</li>
|
||||
<li>${h.link_to("View this Label Profile", url('label_profile.read', uuid=profile.uuid))}</li>
|
||||
<li>${h.link_to("Edit this Label Profile", url('label_profile.update', uuid=profile.uuid))}</li>
|
||||
</%def>
|
||||
|
||||
<div class="form-wrapper">
|
||||
|
||||
<ul class="context-menu">
|
||||
${self.context_menu_items()}
|
||||
</ul>
|
||||
|
||||
<div class="form">
|
||||
|
||||
<div class="field-wrapper">
|
||||
<label>Label Profile</label>
|
||||
<div class="field">${profile.description}</div>
|
||||
</div>
|
||||
|
||||
<div class="field-wrapper">
|
||||
<label>Printer Spec</label>
|
||||
<div class="field">${profile.printer_spec}</div>
|
||||
</div>
|
||||
|
||||
${h.form(request.current_route_url())}
|
||||
|
||||
% for name, display in printer.required_settings.iteritems():
|
||||
<div class="field-wrapper">
|
||||
<label for="${name}">${display}</label>
|
||||
<div class="field">
|
||||
${h.text(name, value=profile.get_printer_setting(name))}
|
||||
</div>
|
||||
</div>
|
||||
% endfor
|
||||
|
||||
<div class="buttons">
|
||||
${h.submit('update', "Update")}
|
||||
<button type="button" onclick="location.href = '${url('label_profile.read', uuid=profile.uuid)}';">Cancel</button>
|
||||
</div>
|
||||
|
||||
${h.end_form()}
|
||||
</div>
|
||||
|
||||
</div>
|
32
rattail/pyramid/templates/labels/profiles/read.mako
Normal file
32
rattail/pyramid/templates/labels/profiles/read.mako
Normal file
|
@ -0,0 +1,32 @@
|
|||
<%inherit file="/labels/profiles/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Label Profiles", url('label_profiles'))}</li>
|
||||
% if form.readonly and request.has_perm('label_profiles.update'):
|
||||
<% profile = form.fieldset.model %>
|
||||
<% printer = profile.get_printer() %>
|
||||
<li>${h.link_to("Edit this Label Profile", url('label_profile.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% if printer and printer.required_settings:
|
||||
<li>${h.link_to("Edit Printer Settings", url('label_profile.printer_settings', uuid=profile.uuid))}</li>
|
||||
% endif
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
||||
|
||||
<% profile = form.fieldset.model %>
|
||||
<% printer = profile.get_printer() %>
|
||||
|
||||
% if printer and printer.required_settings:
|
||||
<h2>Printer Settings</h2>
|
||||
|
||||
<div class="form">
|
||||
% for name, display in printer.required_settings.iteritems():
|
||||
<div class="field-wrapper">
|
||||
<label>${display}</label>
|
||||
<div class="field">${profile.get_printer_setting(name) or ''}</div>
|
||||
</div>
|
||||
% endfor
|
||||
</div>
|
||||
|
||||
% endif
|
27
rattail/pyramid/templates/products/batch.mako
Normal file
27
rattail/pyramid/templates/products/batch.mako
Normal file
|
@ -0,0 +1,27 @@
|
|||
<%inherit file="/base.mako" />
|
||||
|
||||
<%def name="title()">Create Products Batch</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Products", url('products'))}</li>
|
||||
</%def>
|
||||
|
||||
<div class="form">
|
||||
|
||||
${h.form(request.current_route_url())}
|
||||
|
||||
<div class="field-wrapper">
|
||||
<label for="provider">Batch Type</label>
|
||||
<div class="field">
|
||||
${h.select('provider', None, providers)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
${h.submit('create', "Create Batch")}
|
||||
<button type="button" onclick="location.href = '${url('products')}';">Cancel</button>
|
||||
</div>
|
||||
|
||||
${h.end_form()}
|
||||
|
||||
</div>
|
12
rattail/pyramid/templates/products/crud.mako
Normal file
12
rattail/pyramid/templates/products/crud.mako
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Products", url('products'))}</li>
|
||||
% if form.readonly:
|
||||
<li>${h.link_to("Edit this Product", url('product.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this Product", url('product.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
107
rattail/pyramid/templates/products/index.mako
Normal file
107
rattail/pyramid/templates/products/index.mako
Normal file
|
@ -0,0 +1,107 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Products</%def>
|
||||
|
||||
<%def name="head_tags()">
|
||||
${parent.head_tags()}
|
||||
<style type="text/css">
|
||||
|
||||
table.grid-header td.tools table {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
table.grid-header td.tools table th,
|
||||
table.grid-header td.tools table td {
|
||||
padding: 0px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table.grid-header td.tools table #label-quantity {
|
||||
text-align: right;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
div.grid table tbody td.size,
|
||||
div.grid table tbody td.regular_price_uuid,
|
||||
div.grid table tbody td.current_price_uuid {
|
||||
padding-right: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.grid table tbody td.labels {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
% if label_profiles and request.has_perm('products.print_labels'):
|
||||
<script language="javascript" type="text/javascript">
|
||||
|
||||
$(function() {
|
||||
$('div.grid a.print-label').live('click', function() {
|
||||
var quantity = $('#label-quantity').val();
|
||||
if (isNaN(quantity)) {
|
||||
alert("You must provide a valid label quantity.");
|
||||
$('#label-quantity').select();
|
||||
$('#label-quantity').focus();
|
||||
} else {
|
||||
$.ajax({
|
||||
url: '${url('products.print_labels')}',
|
||||
data: {
|
||||
'product': get_uuid(this),
|
||||
'profile': $('#label-profile').val(),
|
||||
'quantity': quantity,
|
||||
},
|
||||
success: function(data) {
|
||||
if (data.error) {
|
||||
alert("An error occurred while attempting to print:\n\n" + data.error);
|
||||
} else if (quantity == '1') {
|
||||
alert("1 label has been printed.");
|
||||
} else {
|
||||
alert(quantity + " labels have been printed.");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="tools()">
|
||||
% if label_profiles and request.has_perm('products.print_labels'):
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Label</td>
|
||||
<td>Qty.</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td>
|
||||
<select name="label-profile" id="label-profile">
|
||||
% for profile in label_profiles:
|
||||
<option value="${profile.uuid}">${profile.description}</option>
|
||||
% endfor
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="label-quantity" id="label-quantity" value="1" />
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('products.create'):
|
||||
<li>${h.link_to("Create a new Product", url('product.create'))}</li>
|
||||
% endif
|
||||
% if request.has_perm('batches.create'):
|
||||
<li>${h.link_to("Create Batch from Results", url('products.create_batch'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
37
rattail/pyramid/templates/products/read.mako
Normal file
37
rattail/pyramid/templates/products/read.mako
Normal file
|
@ -0,0 +1,37 @@
|
|||
<%inherit file="/products/crud.mako" />
|
||||
|
||||
${parent.body()}
|
||||
|
||||
<% product = form.fieldset.model %>
|
||||
|
||||
<div id="product-costs">
|
||||
<h2>Product Costs:</h2>
|
||||
% if product.costs:
|
||||
<div class="grid hoverable">
|
||||
<table>
|
||||
<thead>
|
||||
<th>Pref.</th>
|
||||
<th>Vendor</th>
|
||||
<th>Code</th>
|
||||
<th>Case Size</th>
|
||||
<th>Case Cost</th>
|
||||
<th>Unit Cost</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for i, cost in enumerate(product.costs, 1):
|
||||
<tr class="${'odd' if i % 2 else 'even'}">
|
||||
<td class="center">${'X' if cost.preference == 1 else ''}</td>
|
||||
<td>${cost.vendor}</td>
|
||||
<td class="center">${cost.code}</td>
|
||||
<td class="center">${cost.case_size}</td>
|
||||
<td class="right">${'$ %0.2f' % cost.case_cost if cost.case_cost is not None else ''}</td>
|
||||
<td class="right">${'$ %0.4f' % cost.unit_cost if cost.unit_cost is not None else ''}</td>
|
||||
</tr>
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
% else:
|
||||
<p>None on file.</p>
|
||||
% endif
|
||||
</div>
|
2
rattail/pyramid/templates/reports/base.mako
Normal file
2
rattail/pyramid/templates/reports/base.mako
Normal file
|
@ -0,0 +1,2 @@
|
|||
<%inherit file="/base.mako" />
|
||||
${parent.body()}
|
84
rattail/pyramid/templates/reports/ordering.mako
Normal file
84
rattail/pyramid/templates/reports/ordering.mako
Normal file
|
@ -0,0 +1,84 @@
|
|||
<%inherit file="/reports/base.mako" />
|
||||
|
||||
<%def name="title()">Report : Ordering Worksheet</%def>
|
||||
|
||||
<%def name="head_tags()">
|
||||
${parent.head_tags()}
|
||||
<style type="text/css">
|
||||
|
||||
div.grid {
|
||||
clear: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
<p>Please provide the following criteria to generate your report:</p>
|
||||
<br />
|
||||
|
||||
${h.form(request.current_route_url())}
|
||||
${h.hidden('departments', value='')}
|
||||
|
||||
<div class="field-wrapper">
|
||||
${h.hidden('vendor', value='')}
|
||||
<label for="vendor-name">Vendor:</label>
|
||||
${h.text('vendor-name', size='40', value='')}
|
||||
<div id="vendor-display" style="display: none;">
|
||||
<span>(no vendor)</span>
|
||||
<button type="button" id="change-vendor">Change</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-wrapper">
|
||||
<label>Departments:</label>
|
||||
<div class="grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
${h.submit('submit', "Generate Report")}
|
||||
</div>
|
||||
|
||||
${h.end_form()}
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
$(function() {
|
||||
|
||||
var autocompleter = $('#vendor-name').autocomplete({
|
||||
serviceUrl: '${url('vendors.autocomplete')}',
|
||||
width: 300,
|
||||
onSelect: function(value, data) {
|
||||
$('#vendor').val(data);
|
||||
$('#vendor-name').hide();
|
||||
$('#vendor-name').val('');
|
||||
$('#vendor-display span').html(value);
|
||||
$('#vendor-display').show();
|
||||
loading($('div.grid'));
|
||||
$('div.grid').load('${url('departments.by_vendor')}', {'uuid': data});
|
||||
},
|
||||
});
|
||||
|
||||
$('#vendor-name').focus();
|
||||
|
||||
$('#change-vendor').click(function() {
|
||||
$('#vendor').val('');
|
||||
$('#vendor-display').hide();
|
||||
$('#vendor-name').show();
|
||||
$('#vendor-name').focus();
|
||||
$('div.grid').empty();
|
||||
});
|
||||
|
||||
$('form').submit(function() {
|
||||
var depts = [];
|
||||
$('div.grid table tbody tr').each(function() {
|
||||
if ($(this).find('td.checkbox input[type=checkbox]').is(':checked')) {
|
||||
depts.push(get_uuid(this));
|
||||
}
|
||||
$('#departments').val(depts.toString());
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
7
rattail/pyramid/templates/stores/crud.mako
Normal file
7
rattail/pyramid/templates/stores/crud.mako
Normal file
|
@ -0,0 +1,7 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Stores", url('stores'))}</li>
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
11
rattail/pyramid/templates/stores/index.mako
Normal file
11
rattail/pyramid/templates/stores/index.mako
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Stores</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('stores.create'):
|
||||
<li>${h.link_to("Create a new Store", url('store.create'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
12
rattail/pyramid/templates/subdepartments/crud.mako
Normal file
12
rattail/pyramid/templates/subdepartments/crud.mako
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Subdepartments", url('subdepartments'))}</li>
|
||||
% if form.readonly:
|
||||
<li>${h.link_to("Edit this Subdepartment", url('subdepartment.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this Subdepartment", url('subdepartment.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
11
rattail/pyramid/templates/subdepartments/index.mako
Normal file
11
rattail/pyramid/templates/subdepartments/index.mako
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Subdepartments</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('subdepartments.create'):
|
||||
<li>${h.link_to("Create a new Subdepartment", url('subdepartment.create'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
12
rattail/pyramid/templates/vendors/crud.mako
vendored
Normal file
12
rattail/pyramid/templates/vendors/crud.mako
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
<%inherit file="/crud.mako" />
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
<li>${h.link_to("Back to Vendors", url('vendors'))}</li>
|
||||
% if form.readonly:
|
||||
<li>${h.link_to("Edit this Vendor", url('vendor.update', uuid=form.fieldset.model.uuid))}</li>
|
||||
% elif form.updating:
|
||||
<li>${h.link_to("View this Vendor", url('vendor.read', uuid=form.fieldset.model.uuid))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
11
rattail/pyramid/templates/vendors/index.mako
vendored
Normal file
11
rattail/pyramid/templates/vendors/index.mako
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%inherit file="/grid.mako" />
|
||||
|
||||
<%def name="title()">Vendors</%def>
|
||||
|
||||
<%def name="context_menu_items()">
|
||||
% if request.has_perm('vendors.create'):
|
||||
<li>${h.link_to("Create a new Vendor", url('vendor.create'))}</li>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
${parent.body()}
|
41
rattail/pyramid/views/__init__.py
Normal file
41
rattail/pyramid/views/__init__.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.pyramid.views`` -- Pyramid Views
|
||||
"""
|
||||
|
||||
|
||||
def includeme(config):
|
||||
config.include('rattail.pyramid.views.batches')
|
||||
# config.include('rattail.pyramid.views.categories')
|
||||
config.include('rattail.pyramid.views.customer_groups')
|
||||
config.include('rattail.pyramid.views.customers')
|
||||
config.include('rattail.pyramid.views.departments')
|
||||
config.include('rattail.pyramid.views.employees')
|
||||
config.include('rattail.pyramid.views.labels')
|
||||
config.include('rattail.pyramid.views.products')
|
||||
config.include('rattail.pyramid.views.stores')
|
||||
config.include('rattail.pyramid.views.subdepartments')
|
||||
config.include('rattail.pyramid.views.vendors')
|
35
rattail/pyramid/views/batches/__init__.py
Normal file
35
rattail/pyramid/views/batches/__init__.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.pyramid.views.batches`` -- Batch Views
|
||||
"""
|
||||
|
||||
from rattail.pyramid.views.batches.params import *
|
||||
|
||||
|
||||
def includeme(config):
|
||||
config.include('rattail.pyramid.views.batches.core')
|
||||
config.include('rattail.pyramid.views.batches.params')
|
||||
config.include('rattail.pyramid.views.batches.rows')
|
202
rattail/pyramid/views/batches/core.py
Normal file
202
rattail/pyramid/views/batches/core.py
Normal file
|
@ -0,0 +1,202 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.pyramid.views.batches.core`` -- Core Batch Views
|
||||
"""
|
||||
|
||||
import threading
|
||||
|
||||
from pyramid.httpexceptions import HTTPFound
|
||||
from pyramid.renderers import render_to_response
|
||||
|
||||
from webhelpers.html import tags
|
||||
|
||||
import edbob
|
||||
from edbob.pyramid import Session
|
||||
from edbob.pyramid.forms import EnumFieldRenderer, PrettyDateTimeFieldRenderer
|
||||
from edbob.pyramid.grids.search import BooleanSearchFilter
|
||||
from edbob.pyramid.progress import SessionProgress
|
||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView, View
|
||||
|
||||
import rattail
|
||||
from rattail import batches
|
||||
|
||||
|
||||
class BatchesGrid(SearchableAlchemyGridView):
|
||||
|
||||
mapped_class = rattail.Batch
|
||||
config_prefix = 'batches'
|
||||
sort = 'id'
|
||||
|
||||
def filter_map(self):
|
||||
|
||||
def executed_is(q, v):
|
||||
if v == 'True':
|
||||
return q.filter(rattail.Batch.executed != None)
|
||||
else:
|
||||
return q.filter(rattail.Batch.executed == None)
|
||||
|
||||
def executed_isnot(q, v):
|
||||
if v == 'True':
|
||||
return q.filter(rattail.Batch.executed == None)
|
||||
else:
|
||||
return q.filter(rattail.Batch.executed != None)
|
||||
|
||||
return self.make_filter_map(
|
||||
exact=['id'],
|
||||
ilike=['source', 'destination', 'description'],
|
||||
executed={
|
||||
'is': executed_is,
|
||||
'nt': executed_isnot,
|
||||
})
|
||||
|
||||
def filter_config(self):
|
||||
return self.make_filter_config(
|
||||
filter_label_id="ID",
|
||||
filter_factory_executed=BooleanSearchFilter,
|
||||
include_filter_executed=True,
|
||||
filter_type_executed='is',
|
||||
executed='False')
|
||||
|
||||
def sort_map(self):
|
||||
return self.make_sort_map('source', 'id', 'destination', 'description', 'executed')
|
||||
|
||||
def grid(self):
|
||||
g = self.make_grid()
|
||||
g.executed.set(renderer=PrettyDateTimeFieldRenderer(from_='utc'))
|
||||
g.configure(
|
||||
include=[
|
||||
g.source,
|
||||
g.id.label("ID"),
|
||||
g.destination,
|
||||
g.description,
|
||||
g.rowcount.label("Row Count"),
|
||||
g.executed,
|
||||
],
|
||||
readonly=True)
|
||||
if self.request.has_perm('batches.read'):
|
||||
def rows(row):
|
||||
return tags.link_to("View Rows", self.request.route_url(
|
||||
'batch.rows', uuid=row.uuid))
|
||||
g.add_column('rows', "", rows)
|
||||
g.clickable = True
|
||||
g.click_route_name = 'batch.read'
|
||||
if self.request.has_perm('batches.update'):
|
||||
g.editable = True
|
||||
g.edit_route_name = 'batch.update'
|
||||
if self.request.has_perm('batches.delete'):
|
||||
g.deletable = True
|
||||
g.delete_route_name = 'batch.delete'
|
||||
return g
|
||||
|
||||
|
||||
class BatchCrud(CrudView):
|
||||
|
||||
mapped_class = rattail.Batch
|
||||
home_route = 'batches'
|
||||
|
||||
def fieldset(self, model):
|
||||
fs = self.make_fieldset(model)
|
||||
fs.action_type.set(renderer=EnumFieldRenderer(rattail.BATCH_ACTION))
|
||||
fs.executed.set(renderer=PrettyDateTimeFieldRenderer(from_='utc'))
|
||||
fs.configure(
|
||||
include=[
|
||||
fs.source,
|
||||
fs.id.label("ID"),
|
||||
fs.destination,
|
||||
fs.action_type,
|
||||
fs.description,
|
||||
fs.rowcount.label("Row Count").readonly(),
|
||||
fs.executed.readonly(),
|
||||
])
|
||||
return fs
|
||||
|
||||
def post_delete(self, batch):
|
||||
batch.drop_table()
|
||||
|
||||
|
||||
class ExecuteBatch(View):
|
||||
|
||||
def execute_batch(self, batch, progress):
|
||||
session = edbob.Session()
|
||||
batch = session.merge(batch)
|
||||
|
||||
if not batch.execute(progress):
|
||||
session.rollback()
|
||||
session.close()
|
||||
return
|
||||
|
||||
session.commit()
|
||||
session.refresh(batch)
|
||||
session.close()
|
||||
|
||||
progress.session.load()
|
||||
progress.session['complete'] = True
|
||||
progress.session['success_msg'] = "Batch \"%s\" has been executed." % batch.description
|
||||
progress.session['success_url'] = self.request.route_url('batches')
|
||||
progress.session.save()
|
||||
|
||||
def __call__(self):
|
||||
uuid = self.request.matchdict['uuid']
|
||||
batch = Session.query(rattail.Batch).get(uuid) if uuid else None
|
||||
if not batch:
|
||||
return HTTPFound(location=self.request.route_url('batches'))
|
||||
|
||||
progress = SessionProgress(self.request.session, 'batch.execute')
|
||||
thread = threading.Thread(target=self.execute_batch, args=(batch, progress))
|
||||
thread.start()
|
||||
kwargs = {
|
||||
'key': 'batch.execute',
|
||||
'cancel_url': self.request.route_url('batch.rows', uuid=batch.uuid),
|
||||
'cancel_msg': "Batch execution was canceled.",
|
||||
}
|
||||
return render_to_response('/progress.mako', kwargs, request=self.request)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
||||
config.add_route('batches', '/batches')
|
||||
config.add_view(BatchesGrid, route_name='batches',
|
||||
renderer='/batches/index.mako',
|
||||
permission='batches.list')
|
||||
|
||||
config.add_route('batch.read', '/batches/{uuid}')
|
||||
config.add_view(BatchCrud, attr='read',
|
||||
route_name='batch.read',
|
||||
renderer='/batches/read.mako',
|
||||
permission='batches.read')
|
||||
|
||||
config.add_route('batch.update', '/batches/{uuid}/edit')
|
||||
config.add_view(BatchCrud, attr='update', route_name='batch.update',
|
||||
renderer='/batches/crud.mako',
|
||||
permission='batches.update')
|
||||
|
||||
config.add_route('batch.delete', '/batches/{uuid}/delete')
|
||||
config.add_view(BatchCrud, attr='delete', route_name='batch.delete',
|
||||
permission='batches.delete')
|
||||
|
||||
config.add_route('batch.execute', '/batches/{uuid}/execute')
|
||||
config.add_view(ExecuteBatch, route_name='batch.execute',
|
||||
permission='batches.execute')
|
52
rattail/pyramid/views/batches/params/__init__.py
Normal file
52
rattail/pyramid/views/batches/params/__init__.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.pyramid.views.batches.params`` -- Batch Parameter Views
|
||||
"""
|
||||
|
||||
from edbob.pyramid.views import View
|
||||
|
||||
|
||||
__all__ = ['BatchParamsView']
|
||||
|
||||
|
||||
class BatchParamsView(View):
|
||||
|
||||
provider_name = None
|
||||
|
||||
def render_kwargs(self):
|
||||
return {}
|
||||
|
||||
def __call__(self):
|
||||
if self.request.POST:
|
||||
if self.set_batch_params():
|
||||
return HTTPFound(location=self.request.get_referer())
|
||||
kwargs = self.render_kwargs()
|
||||
kwargs['provider'] = self.provider_name
|
||||
return kwargs
|
||||
|
||||
|
||||
def includeme(config):
|
||||
config.include('rattail.pyramid.views.batches.params.labels')
|
51
rattail/pyramid/views/batches/params/labels.py
Normal file
51
rattail/pyramid/views/batches/params/labels.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.pyramid.views.batches.params.printlabels`` -- Print Labels Batch
|
||||
"""
|
||||
|
||||
from edbob.pyramid import Session
|
||||
|
||||
import rattail
|
||||
from rattail.pyramid.views.batches.params import BatchParamsView
|
||||
|
||||
|
||||
class PrintLabels(BatchParamsView):
|
||||
|
||||
provider_name = 'print_labels'
|
||||
|
||||
def render_kwargs(self):
|
||||
q = Session.query(rattail.LabelProfile)
|
||||
q = q.order_by(rattail.LabelProfile.ordinal)
|
||||
profiles = [(x.code, x.description) for x in q]
|
||||
return {'label_profiles': profiles}
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
||||
config.add_route('batch_params.print_labels', '/batches/params/print-labels')
|
||||
config.add_view(PrintLabels, route_name='batch_params.print_labels',
|
||||
renderer='/batches/params/print_labels.mako',
|
||||
permission='batches.print_labels')
|
220
rattail/pyramid/views/batches/rows.py
Normal file
220
rattail/pyramid/views/batches/rows.py
Normal file
|
@ -0,0 +1,220 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.pyramid.views.batches.rows`` -- Batch Row Views
|
||||
"""
|
||||
|
||||
from pyramid.httpexceptions import HTTPFound
|
||||
|
||||
from edbob.pyramid import Session
|
||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||
|
||||
import rattail
|
||||
from rattail.pyramid.forms import GPCFieldRenderer
|
||||
|
||||
|
||||
def field_with_renderer(field, column):
|
||||
|
||||
if column.sil_name == 'F01': # UPC
|
||||
field = field.with_renderer(GPCFieldRenderer)
|
||||
|
||||
elif column.sil_name == 'F95': # Shelf Tag Type
|
||||
q = Session.query(rattail.LabelProfile)
|
||||
q = q.order_by(rattail.LabelProfile.ordinal)
|
||||
field = field.dropdown(options=[(x.description, x.code) for x in q])
|
||||
|
||||
return field
|
||||
|
||||
|
||||
def BatchRowsGrid(request):
|
||||
uuid = request.matchdict['uuid']
|
||||
batch = Session.query(rattail.Batch).get(uuid) if uuid else None
|
||||
if not batch:
|
||||
return HTTPFound(location=request.route_url('batches'))
|
||||
|
||||
class BatchRowsGrid(SearchableAlchemyGridView):
|
||||
|
||||
mapped_class = batch.rowclass
|
||||
config_prefix = 'batch.%s' % batch.uuid
|
||||
sort = 'ordinal'
|
||||
|
||||
def filter_map(self):
|
||||
fmap = self.make_filter_map()
|
||||
for column in batch.columns:
|
||||
if column.visible:
|
||||
if column.data_type.startswith('CHAR'):
|
||||
fmap[column.name] = self.filter_ilike(
|
||||
getattr(batch.rowclass, column.name))
|
||||
else:
|
||||
fmap[column.name] = self.filter_exact(
|
||||
getattr(batch.rowclass, column.name))
|
||||
return fmap
|
||||
|
||||
def filter_config(self):
|
||||
config = self.make_filter_config()
|
||||
for column in batch.columns:
|
||||
if column.visible:
|
||||
config['filter_label_%s' % column.name] = column.display_name
|
||||
return config
|
||||
|
||||
def grid(self):
|
||||
g = self.make_grid()
|
||||
|
||||
include = [g.ordinal.label("Row")]
|
||||
for column in batch.columns:
|
||||
if column.visible:
|
||||
field = getattr(g, column.name)
|
||||
field = field_with_renderer(field, column)
|
||||
field = field.label(column.display_name)
|
||||
include.append(field)
|
||||
g.column_titles[field.key] = '%s - %s - %s' % (
|
||||
column.sil_name, column.description, column.data_type)
|
||||
|
||||
g.configure(include=include, readonly=True)
|
||||
|
||||
route_kwargs = lambda x: {'batch_uuid': x.batch.uuid, 'uuid': x.uuid}
|
||||
|
||||
if self.request.has_perm('batch_rows.read'):
|
||||
g.clickable = True
|
||||
g.click_route_name = 'batch_row.read'
|
||||
g.click_route_kwargs = route_kwargs
|
||||
|
||||
if self.request.has_perm('batch_rows.update'):
|
||||
g.editable = True
|
||||
g.edit_route_name = 'batch_row.update'
|
||||
g.edit_route_kwargs = route_kwargs
|
||||
|
||||
if self.request.has_perm('batch_rows.delete'):
|
||||
g.deletable = True
|
||||
g.delete_route_name = 'batch_row.delete'
|
||||
g.delete_route_kwargs = route_kwargs
|
||||
|
||||
return g
|
||||
|
||||
def render_kwargs(self):
|
||||
return {'batch': batch}
|
||||
|
||||
grid = BatchRowsGrid(request)
|
||||
grid.batch = batch
|
||||
return grid
|
||||
|
||||
|
||||
def batch_rows_grid(request):
|
||||
grid = BatchRowsGrid(request)
|
||||
return grid()
|
||||
|
||||
|
||||
def batch_rows_delete(request):
|
||||
grid = BatchRowsGrid(request)
|
||||
grid._filter_config = grid.filter_config()
|
||||
rows = grid.make_query()
|
||||
count = rows.count()
|
||||
rows.delete(synchronize_session=False)
|
||||
grid.batch.rowcount -= count
|
||||
request.session.flash("Deleted %d rows from batch." % count)
|
||||
return HTTPFound(location=request.route_url('batch.rows', uuid=grid.batch.uuid))
|
||||
|
||||
|
||||
def batch_row_crud(request, attr):
|
||||
batch_uuid = request.matchdict['batch_uuid']
|
||||
batch = Session.query(rattail.Batch).get(batch_uuid)
|
||||
if not batch:
|
||||
return HTTPFound(location=request.route_url('batches'))
|
||||
|
||||
row_uuid = request.matchdict['uuid']
|
||||
row = Session.query(batch.rowclass).get(row_uuid)
|
||||
if not row:
|
||||
return HTTPFound(location=request.route_url('batch', uuid=batch.uuid))
|
||||
|
||||
class BatchRowCrud(CrudView):
|
||||
|
||||
mapped_class = batch.rowclass
|
||||
pretty_name = "Batch Row"
|
||||
|
||||
@property
|
||||
def home_url(self):
|
||||
return self.request.route_url('batch.rows', uuid=batch.uuid)
|
||||
|
||||
@property
|
||||
def cancel_url(self):
|
||||
return self.home_url
|
||||
|
||||
def fieldset(self, model):
|
||||
fs = self.make_fieldset(model)
|
||||
|
||||
include = [fs.ordinal.label("Row Number").readonly()]
|
||||
for column in batch.columns:
|
||||
field = getattr(fs, column.name)
|
||||
field = field_with_renderer(field, column)
|
||||
field = field.label(column.display_name)
|
||||
include.append(field)
|
||||
|
||||
fs.configure(include=include)
|
||||
return fs
|
||||
|
||||
def flash_delete(self, row):
|
||||
self.request.session.flash("Batch Row %d has been deleted."
|
||||
% row.ordinal)
|
||||
|
||||
def post_delete(self, model):
|
||||
batch.rowcount -= 1
|
||||
|
||||
crud = BatchRowCrud(request)
|
||||
return getattr(crud, attr)()
|
||||
|
||||
def batch_row_read(request):
|
||||
return batch_row_crud(request, 'read')
|
||||
|
||||
def batch_row_update(request):
|
||||
return batch_row_crud(request, 'update')
|
||||
|
||||
def batch_row_delete(request):
|
||||
return batch_row_crud(request, 'delete')
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
||||
config.add_route('batch.rows', '/batches/{uuid}/rows')
|
||||
config.add_view(batch_rows_grid, route_name='batch.rows',
|
||||
renderer='/batches/rows/index.mako',
|
||||
permission='batches.read')
|
||||
|
||||
config.add_route('batch.rows.delete', '/batches/{uuid}/rows/delete')
|
||||
config.add_view(batch_rows_delete, route_name='batch.rows.delete',
|
||||
permission='batch_rows.delete')
|
||||
|
||||
config.add_route('batch_row.read', '/batches/{batch_uuid}/{uuid}')
|
||||
config.add_view(batch_row_read, route_name='batch_row.read',
|
||||
renderer='/batches/rows/crud.mako',
|
||||
permission='batch_rows.read')
|
||||
|
||||
config.add_route('batch_row.update', '/batches/{batch_uuid}/{uuid}/edit')
|
||||
config.add_view(batch_row_update, route_name='batch_row.update',
|
||||
renderer='/batches/rows/crud.mako',
|
||||
permission='batch_rows.update')
|
||||
|
||||
config.add_route('batch_row.delete', '/batches/{batch_uuid}/{uuid}/delete')
|
||||
config.add_view(batch_row_delete, route_name='batch_row.delete',
|
||||
permission='batch_rows.delete')
|
126
rattail/pyramid/views/brands.py
Normal file
126
rattail/pyramid/views/brands.py
Normal file
|
@ -0,0 +1,126 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.pyramid.views.brands`` -- Brand Views
|
||||
"""
|
||||
|
||||
from edbob.pyramid.views import (
|
||||
SearchableAlchemyGridView, CrudView, AutocompleteView)
|
||||
|
||||
import rattail
|
||||
|
||||
|
||||
class BrandsGrid(SearchableAlchemyGridView):
|
||||
|
||||
mapped_class = rattail.Brand
|
||||
config_prefix = 'brands'
|
||||
sort = 'name'
|
||||
|
||||
def filter_map(self):
|
||||
return self.make_filter_map(ilike=['name'])
|
||||
|
||||
def filter_config(self):
|
||||
return self.make_filter_config(
|
||||
include_filter_name=True,
|
||||
filter_type_name='lk')
|
||||
|
||||
def sort_map(self):
|
||||
return self.make_sort_map('name')
|
||||
|
||||
def grid(self):
|
||||
g = self.make_grid()
|
||||
g.configure(
|
||||
include=[
|
||||
g.name,
|
||||
],
|
||||
readonly=True)
|
||||
if self.request.has_perm('brands.read'):
|
||||
g.clickable = True
|
||||
g.click_route_name = 'brand.read'
|
||||
if self.request.has_perm('brands.update'):
|
||||
g.editable = True
|
||||
g.edit_route_name = 'brand.update'
|
||||
if self.request.has_perm('brands.delete'):
|
||||
g.deletable = True
|
||||
g.delete_route_name = 'brand.delete'
|
||||
return g
|
||||
|
||||
|
||||
class BrandCrud(CrudView):
|
||||
|
||||
mapped_class = rattail.Brand
|
||||
home_route = 'brands'
|
||||
|
||||
def fieldset(self, model):
|
||||
fs = self.make_fieldset(model)
|
||||
fs.configure(
|
||||
include=[
|
||||
fs.name,
|
||||
])
|
||||
return fs
|
||||
|
||||
|
||||
class BrandsAutocomplete(AutocompleteView):
|
||||
|
||||
mapped_class = rattail.Brand
|
||||
fieldname = 'name'
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
||||
config.add_route('brands', '/brands')
|
||||
config.add_view(BrandsGrid,
|
||||
route_name='brands',
|
||||
renderer='/brands/index.mako',
|
||||
permission='brands.list')
|
||||
|
||||
config.add_route('brands.autocomplete', '/brands/autocomplete')
|
||||
config.add_view(BrandsAutocomplete,
|
||||
route_name='brands.autocomplete',
|
||||
renderer='json',
|
||||
permission='brands.list')
|
||||
|
||||
config.add_route('brand.create', '/brands/new')
|
||||
config.add_view(BrandCrud, attr='create',
|
||||
route_name='brand.create',
|
||||
renderer='/brands/crud.mako',
|
||||
permission='brands.create')
|
||||
|
||||
config.add_route('brand.read', '/brands/{uuid}')
|
||||
config.add_view(BrandCrud, attr='read',
|
||||
route_name='brand.read',
|
||||
renderer='/brands/crud.mako',
|
||||
permission='brands.read')
|
||||
|
||||
config.add_route('brand.update', '/brands/{uuid}/edit')
|
||||
config.add_view(BrandCrud, attr='update',
|
||||
route_name='brand.update',
|
||||
renderer='/brands/crud.mako',
|
||||
permission='brands.update')
|
||||
|
||||
config.add_route('brand.delete', '/brands/{uuid}/delete')
|
||||
config.add_view(BrandCrud, attr='delete',
|
||||
route_name='brand.delete',
|
||||
permission='brands.delete')
|
110
rattail/pyramid/views/categories.py
Normal file
110
rattail/pyramid/views/categories.py
Normal file
|
@ -0,0 +1,110 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.pyramid.views.categories`` -- Category Views
|
||||
"""
|
||||
|
||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||
|
||||
import rattail
|
||||
|
||||
|
||||
class CategoriesGrid(SearchableAlchemyGridView):
|
||||
|
||||
mapped_class = rattail.Category
|
||||
config_prefix = 'categories'
|
||||
sort = 'number'
|
||||
|
||||
def filter_map(self):
|
||||
return self.make_filter_map(exact=['number'], ilike=['name'])
|
||||
|
||||
def filter_config(self):
|
||||
return self.make_filter_config(
|
||||
include_filter_name=True,
|
||||
filter_type_name='lk')
|
||||
|
||||
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)
|
||||
if self.request.has_perm('categories.read'):
|
||||
g.clickable = True
|
||||
g.click_route_name = 'category.read'
|
||||
if self.request.has_perm('categories.update'):
|
||||
g.editable = True
|
||||
g.edit_route_name = 'category.update'
|
||||
if self.request.has_perm('categories.delete'):
|
||||
g.deletable = True
|
||||
g.delete_route_name = 'category.delete'
|
||||
return g
|
||||
|
||||
|
||||
class CategoryCrud(CrudView):
|
||||
|
||||
mapped_class = rattail.Category
|
||||
home_route = 'categories'
|
||||
|
||||
def fieldset(self, model):
|
||||
fs = self.make_fieldset(model)
|
||||
fs.configure(
|
||||
include=[
|
||||
fs.number,
|
||||
fs.name,
|
||||
])
|
||||
return fs
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
||||
config.add_route('categories', '/categories')
|
||||
config.add_view(CategoriesGrid, route_name='categories',
|
||||
renderer='/categories/index.mako',
|
||||
permission='categories.list')
|
||||
|
||||
config.add_route('category.create', '/categories/new')
|
||||
config.add_view(CategoryCrud, attr='create', route_name='category.create',
|
||||
renderer='/categories/crud.mako',
|
||||
permission='categories.create')
|
||||
|
||||
config.add_route('category.read', '/categories/{uuid}')
|
||||
config.add_view(CategoryCrud, attr='read', route_name='category.read',
|
||||
renderer='/categories/crud.mako',
|
||||
permission='categories.read')
|
||||
|
||||
config.add_route('category.update', '/categories/{uuid}/edit')
|
||||
config.add_view(CategoryCrud, attr='update', route_name='category.update',
|
||||
renderer='/categories/crud.mako',
|
||||
permission='categories.update')
|
||||
|
||||
config.add_route('category.delete', '/categories/{uuid}/delete')
|
||||
config.add_view(CategoryCrud, attr='delete', route_name='category.delete',
|
||||
permission='categories.delete')
|
88
rattail/pyramid/views/customer_groups.py
Normal file
88
rattail/pyramid/views/customer_groups.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.pyramid.views.customergroups`` -- CustomerGroup Views
|
||||
"""
|
||||
|
||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||
|
||||
import rattail
|
||||
|
||||
|
||||
class CustomerGroupsGrid(SearchableAlchemyGridView):
|
||||
|
||||
mapped_class = rattail.CustomerGroup
|
||||
config_prefix = 'customer_groups'
|
||||
sort = 'name'
|
||||
|
||||
def filter_map(self):
|
||||
return self.make_filter_map(ilike=['name'])
|
||||
|
||||
def filter_config(self):
|
||||
return self.make_filter_config(
|
||||
include_filter_name=True,
|
||||
filter_type_name='lk')
|
||||
|
||||
def sort_map(self):
|
||||
return self.make_sort_map('id', 'name')
|
||||
|
||||
def grid(self):
|
||||
g = self.make_grid()
|
||||
g.configure(
|
||||
include=[
|
||||
g.id.label("ID"),
|
||||
g.name,
|
||||
],
|
||||
readonly=True)
|
||||
return g
|
||||
|
||||
|
||||
class CustomerGroupCrud(CrudView):
|
||||
|
||||
mapped_class = rattail.CustomerGroup
|
||||
home_route = 'customer_groups'
|
||||
pretty_name = "Customer Group"
|
||||
|
||||
def fieldset(self, model):
|
||||
fs = self.make_fieldset(model)
|
||||
fs.configure(
|
||||
include=[
|
||||
fs.id.label("ID"),
|
||||
fs.name,
|
||||
])
|
||||
return fs
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
||||
config.add_route('customer_groups', '/customer-groups')
|
||||
config.add_view(CustomerGroupsGrid, route_name='customer_groups',
|
||||
renderer='/customer_groups/index.mako',
|
||||
permission='customer_groups.list')
|
||||
|
||||
config.add_route('customer_group.read', '/customer-groups/{uuid}')
|
||||
config.add_view(CustomerGroupCrud, attr='read', route_name='customer_group.read',
|
||||
renderer='/customer_groups/crud.mako',
|
||||
permission='customer_groups.read')
|
121
rattail/pyramid/views/customers.py
Normal file
121
rattail/pyramid/views/customers.py
Normal file
|
@ -0,0 +1,121 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.pyramid.views.customers`` -- Customer Views
|
||||
"""
|
||||
|
||||
from sqlalchemy import and_
|
||||
|
||||
import edbob
|
||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||
from edbob.pyramid.forms import EnumFieldRenderer
|
||||
|
||||
import rattail
|
||||
|
||||
|
||||
class CustomersGrid(SearchableAlchemyGridView):
|
||||
|
||||
mapped_class = rattail.Customer
|
||||
config_prefix = 'customers'
|
||||
sort = 'name'
|
||||
clickable = True
|
||||
|
||||
def join_map(self):
|
||||
return {
|
||||
'email':
|
||||
lambda q: q.outerjoin(rattail.CustomerEmailAddress, and_(
|
||||
rattail.CustomerEmailAddress.parent_uuid == rattail.Customer.uuid,
|
||||
rattail.CustomerEmailAddress.preference == 1)),
|
||||
'phone':
|
||||
lambda q: q.outerjoin(rattail.CustomerPhoneNumber, and_(
|
||||
rattail.CustomerPhoneNumber.parent_uuid == rattail.Customer.uuid,
|
||||
rattail.CustomerPhoneNumber.preference == 1)),
|
||||
}
|
||||
|
||||
def filter_map(self):
|
||||
return self.make_filter_map(
|
||||
exact=['id'],
|
||||
ilike=['name'],
|
||||
email=self.filter_ilike(rattail.CustomerEmailAddress.address),
|
||||
phone=self.filter_ilike(rattail.CustomerPhoneNumber.number))
|
||||
|
||||
def filter_config(self):
|
||||
return self.make_filter_config(
|
||||
include_filter_name=True,
|
||||
filter_type_name='lk',
|
||||
filter_label_phone="Phone Number",
|
||||
filter_label_email="Email Address",
|
||||
filter_label_id="ID")
|
||||
|
||||
def sort_map(self):
|
||||
return self.make_sort_map(
|
||||
'id', 'name',
|
||||
email=self.sorter(rattail.CustomerEmailAddress.address),
|
||||
phone=self.sorter(rattail.CustomerPhoneNumber.number))
|
||||
|
||||
def grid(self):
|
||||
g = self.make_grid()
|
||||
g.configure(
|
||||
include=[
|
||||
g.id.label("ID"),
|
||||
g.name,
|
||||
g.phone.label("Phone Number"),
|
||||
g.email.label("Email Address"),
|
||||
],
|
||||
readonly=True)
|
||||
g.click_route_name = 'customer.read'
|
||||
return g
|
||||
|
||||
|
||||
class CustomerCrud(CrudView):
|
||||
|
||||
mapped_class = rattail.Customer
|
||||
home_route = 'customers'
|
||||
|
||||
def fieldset(self, model):
|
||||
fs = self.make_fieldset(model)
|
||||
fs.email_preference.set(renderer=EnumFieldRenderer(edbob.EMAIL_PREFERENCE))
|
||||
fs.configure(
|
||||
include=[
|
||||
fs.id.label("ID"),
|
||||
fs.name,
|
||||
fs.phone.label("Phone Number"),
|
||||
fs.email.label("Email Address"),
|
||||
fs.email_preference,
|
||||
])
|
||||
return fs
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
||||
config.add_route('customers', '/customers')
|
||||
config.add_view(CustomersGrid, route_name='customers',
|
||||
renderer='/customers/index.mako',
|
||||
permission='customers.list')
|
||||
|
||||
config.add_route('customer.read', '/customers/{uuid}')
|
||||
config.add_view(CustomerCrud, attr='read', route_name='customer.read',
|
||||
renderer='/customers/read.mako',
|
||||
permission='customers.read')
|
161
rattail/pyramid/views/departments.py
Normal file
161
rattail/pyramid/views/departments.py
Normal file
|
@ -0,0 +1,161 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.pyramid.views.departments`` -- Department Views
|
||||
"""
|
||||
|
||||
|
||||
from edbob.pyramid.views import (
|
||||
SearchableAlchemyGridView, CrudView, AlchemyGridView, AutocompleteView)
|
||||
|
||||
import rattail
|
||||
|
||||
|
||||
class DepartmentsGrid(SearchableAlchemyGridView):
|
||||
|
||||
mapped_class = rattail.Department
|
||||
config_prefix = 'departments'
|
||||
sort = 'name'
|
||||
|
||||
def filter_map(self):
|
||||
return self.make_filter_map(ilike=['name'])
|
||||
|
||||
def filter_config(self):
|
||||
return self.make_filter_config(
|
||||
include_filter_name=True,
|
||||
filter_type_name='lk')
|
||||
|
||||
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)
|
||||
if self.request.has_perm('departments.read'):
|
||||
g.clickable = True
|
||||
g.click_route_name = 'department.read'
|
||||
if self.request.has_perm('departments.update'):
|
||||
g.editable = True
|
||||
g.edit_route_name = 'department.update'
|
||||
if self.request.has_perm('departments.delete'):
|
||||
g.deletable = True
|
||||
g.delete_route_name = 'department.delete'
|
||||
return g
|
||||
|
||||
|
||||
class DepartmentCrud(CrudView):
|
||||
|
||||
mapped_class = rattail.Department
|
||||
home_route = 'departments'
|
||||
|
||||
def fieldset(self, model):
|
||||
fs = self.make_fieldset(model)
|
||||
fs.configure(
|
||||
include=[
|
||||
fs.number,
|
||||
fs.name,
|
||||
])
|
||||
return fs
|
||||
|
||||
|
||||
class DepartmentsByVendorGrid(AlchemyGridView):
|
||||
|
||||
mapped_class = rattail.Department
|
||||
config_prefix = '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
|
||||
|
||||
def grid(self):
|
||||
g = self.make_grid()
|
||||
g.configure(
|
||||
include=[
|
||||
g.name,
|
||||
],
|
||||
readonly=True)
|
||||
return g
|
||||
|
||||
|
||||
class DepartmentsAutocomplete(AutocompleteView):
|
||||
|
||||
mapped_class = rattail.Department
|
||||
fieldname = 'name'
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
||||
config.add_route('departments', '/departments')
|
||||
config.add_view(DepartmentsGrid,
|
||||
route_name='departments',
|
||||
renderer='/departments/index.mako',
|
||||
permission='departments.list')
|
||||
|
||||
config.add_route('departments.autocomplete', '/departments/autocomplete')
|
||||
config.add_view(DepartmentsAutocomplete,
|
||||
route_name='departments.autocomplete',
|
||||
renderer='json',
|
||||
permission='departments.list')
|
||||
|
||||
config.add_route('departments.by_vendor', '/departments/by-vendor')
|
||||
config.add_view(DepartmentsByVendorGrid,
|
||||
route_name='departments.by_vendor',
|
||||
permission='departments.list')
|
||||
|
||||
config.add_route('department.create', '/departments/new')
|
||||
config.add_view(DepartmentCrud, attr='create',
|
||||
route_name='department.create',
|
||||
renderer='/departments/crud.mako',
|
||||
permission='departments.create')
|
||||
|
||||
config.add_route('department.read', '/departments/{uuid}')
|
||||
config.add_view(DepartmentCrud, attr='read',
|
||||
route_name='department.read',
|
||||
renderer='/departments/crud.mako',
|
||||
permission='departments.read')
|
||||
|
||||
config.add_route('department.update', '/departments/{uuid}/edit')
|
||||
config.add_view(DepartmentCrud, attr='update',
|
||||
route_name='department.update',
|
||||
renderer='/departments/crud.mako',
|
||||
permission='departments.update')
|
||||
|
||||
config.add_route('department.delete', '/departments/{uuid}/delete')
|
||||
config.add_view(DepartmentCrud, attr='delete',
|
||||
route_name='department.delete',
|
||||
permission='departments.delete')
|
98
rattail/pyramid/views/employees.py
Normal file
98
rattail/pyramid/views/employees.py
Normal file
|
@ -0,0 +1,98 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.pyramid.views.employees`` -- Employee Views
|
||||
"""
|
||||
|
||||
from sqlalchemy import and_
|
||||
|
||||
import edbob
|
||||
from edbob.pyramid.forms import AssociationProxyField
|
||||
from edbob.pyramid.views import SearchableAlchemyGridView
|
||||
|
||||
import rattail
|
||||
|
||||
|
||||
class EmployeesGrid(SearchableAlchemyGridView):
|
||||
|
||||
mapped_class = rattail.Employee
|
||||
config_prefix = 'employees'
|
||||
sort = 'first_name'
|
||||
|
||||
def join_map(self):
|
||||
return {
|
||||
'phone':
|
||||
lambda q: q.outerjoin(rattail.EmployeePhoneNumber, and_(
|
||||
rattail.EmployeePhoneNumber.parent_uuid == rattail.Employee.uuid,
|
||||
rattail.EmployeePhoneNumber.preference == 1)),
|
||||
}
|
||||
|
||||
def filter_map(self):
|
||||
return self.make_filter_map(
|
||||
first_name=self.filter_ilike(edbob.Person.first_name),
|
||||
last_name=self.filter_ilike(edbob.Person.last_name),
|
||||
phone=self.filter_ilike(rattail.EmployeePhoneNumber.number))
|
||||
|
||||
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',
|
||||
filter_label_phone="Phone Number")
|
||||
|
||||
def sort_map(self):
|
||||
return self.make_sort_map(
|
||||
first_name=self.sorter(edbob.Person.first_name),
|
||||
last_name=self.sorter(edbob.Person.last_name),
|
||||
phone=self.sorter(rattail.EmployeePhoneNumber.number))
|
||||
|
||||
def query(self):
|
||||
q = self.make_query()
|
||||
q = q.join(edbob.Person)
|
||||
if not self.request.has_perm('employees.edit'):
|
||||
q = q.filter(rattail.Employee.status == rattail.EMPLOYEE_STATUS_CURRENT)
|
||||
return q
|
||||
|
||||
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.phone.label("Phone Number"),
|
||||
],
|
||||
readonly=True)
|
||||
return g
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
||||
config.add_route('employees', '/employees')
|
||||
config.add_view(EmployeesGrid, route_name='employees',
|
||||
renderer='/employees/index.mako',
|
||||
permission='employees.list')
|
189
rattail/pyramid/views/labels.py
Normal file
189
rattail/pyramid/views/labels.py
Normal file
|
@ -0,0 +1,189 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.pyramid.views.labels`` -- Label Views
|
||||
"""
|
||||
|
||||
from pyramid.httpexceptions import HTTPFound
|
||||
|
||||
import formalchemy
|
||||
|
||||
from webhelpers.html import HTML
|
||||
|
||||
from edbob.pyramid import Session
|
||||
from edbob.pyramid.grids.search import BooleanSearchFilter
|
||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||
|
||||
import rattail
|
||||
|
||||
|
||||
class ProfilesGrid(SearchableAlchemyGridView):
|
||||
|
||||
mapped_class = rattail.LabelProfile
|
||||
config_prefix = 'label_profiles'
|
||||
sort = 'ordinal'
|
||||
|
||||
def filter_map(self):
|
||||
return self.make_filter_map(
|
||||
exact=['code', 'visible'],
|
||||
ilike=['description'])
|
||||
|
||||
def filter_config(self):
|
||||
return self.make_filter_config(
|
||||
filter_factory_visible=BooleanSearchFilter)
|
||||
|
||||
def sort_map(self):
|
||||
return self.make_sort_map('ordinal', 'code', 'description', 'visible')
|
||||
|
||||
def grid(self):
|
||||
g = self.make_grid()
|
||||
g.configure(
|
||||
include=[
|
||||
g.ordinal,
|
||||
g.code,
|
||||
g.description,
|
||||
g.visible,
|
||||
],
|
||||
readonly=True)
|
||||
if self.request.has_perm('label_profiles.read'):
|
||||
g.clickable = True
|
||||
g.click_route_name = 'label_profile.read'
|
||||
if self.request.has_perm('label_profiles.update'):
|
||||
g.editable = True
|
||||
g.edit_route_name = 'label_profile.update'
|
||||
if self.request.has_perm('label_profiles.delete'):
|
||||
g.deletable = True
|
||||
g.delete_route_name = 'label_profile.delete'
|
||||
return g
|
||||
|
||||
|
||||
class ProfileCrud(CrudView):
|
||||
|
||||
mapped_class = rattail.LabelProfile
|
||||
home_route = 'label_profiles'
|
||||
pretty_name = "Label Profile"
|
||||
update_cancel_route = 'label_profile.read'
|
||||
|
||||
def fieldset(self, model):
|
||||
|
||||
class FormatFieldRenderer(formalchemy.TextAreaFieldRenderer):
|
||||
|
||||
def render_readonly(self, **kwargs):
|
||||
value = self.raw_value
|
||||
if not value:
|
||||
return ''
|
||||
return HTML.tag('pre', c=value)
|
||||
|
||||
def render(self, **kwargs):
|
||||
kwargs.setdefault('size', (80, 8))
|
||||
return super(FormatFieldRenderer, self).render(**kwargs)
|
||||
|
||||
fs = self.make_fieldset(model)
|
||||
fs.format.set(renderer=FormatFieldRenderer)
|
||||
fs.configure(
|
||||
include=[
|
||||
fs.ordinal,
|
||||
fs.code,
|
||||
fs.description,
|
||||
fs.printer_spec,
|
||||
fs.formatter_spec,
|
||||
fs.format,
|
||||
fs.visible,
|
||||
])
|
||||
return fs
|
||||
|
||||
def post_save(self, form):
|
||||
profile = form.fieldset.model
|
||||
if not profile.format:
|
||||
formatter = profile.get_formatter()
|
||||
if formatter:
|
||||
try:
|
||||
profile.format = formatter.default_format
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
def post_save_url(self, form):
|
||||
return self.request.route_url('label_profile.read',
|
||||
uuid=form.fieldset.model.uuid)
|
||||
|
||||
|
||||
def printer_settings(request):
|
||||
uuid = request.matchdict['uuid']
|
||||
profile = Session.query(rattail.LabelProfile).get(uuid) if uuid else None
|
||||
if not profile:
|
||||
return HTTPFound(location=request.route_url('label_profiles'))
|
||||
|
||||
read_profile = HTTPFound(location=request.route_url(
|
||||
'label_profile.read', uuid=profile.uuid))
|
||||
|
||||
printer = profile.get_printer()
|
||||
if not printer:
|
||||
request.session.flash("Label profile \"%s\" does not have a functional "
|
||||
"printer spec." % profile)
|
||||
return read_profile
|
||||
if not printer.required_settings:
|
||||
request.session.flash("Printer class for label profile \"%s\" does not "
|
||||
"require any settings." % profile)
|
||||
return read_profile
|
||||
|
||||
if request.POST:
|
||||
for setting in printer.required_settings:
|
||||
if setting in request.POST:
|
||||
profile.save_printer_setting(setting, request.POST[setting])
|
||||
return read_profile
|
||||
|
||||
return {'profile': profile, 'printer': printer}
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
||||
config.add_route('label_profiles', '/labels/profiles')
|
||||
config.add_view(ProfilesGrid, route_name='label_profiles',
|
||||
renderer='/labels/profiles/index.mako',
|
||||
permission='label_profiles.list')
|
||||
|
||||
config.add_route('label_profile.create', '/labels/profiles/new')
|
||||
config.add_view(ProfileCrud, attr='create', route_name='label_profile.create',
|
||||
renderer='/labels/profiles/crud.mako',
|
||||
permission='label_profiles.create')
|
||||
|
||||
config.add_route('label_profile.read', '/labels/profiles/{uuid}')
|
||||
config.add_view(ProfileCrud, attr='read', route_name='label_profile.read',
|
||||
renderer='/labels/profiles/read.mako',
|
||||
permission='label_profiles.read')
|
||||
|
||||
config.add_route('label_profile.update', '/labels/profiles/{uuid}/edit')
|
||||
config.add_view(ProfileCrud, attr='update', route_name='label_profile.update',
|
||||
renderer='/labels/profiles/crud.mako',
|
||||
permission='label_profiles.update')
|
||||
|
||||
config.add_route('label_profile.delete', '/labels/profiles/{uuid}/delete')
|
||||
config.add_view(ProfileCrud, attr='delete', route_name='label_profile.delete',
|
||||
permission='label_profiles.delete')
|
||||
|
||||
config.add_route('label_profile.printer_settings', '/labels/profiles/{uuid}/printer')
|
||||
config.add_view(printer_settings, route_name='label_profile.printer_settings',
|
||||
renderer='/labels/profiles/printer.mako',
|
||||
permission='label_profiles.update')
|
349
rattail/pyramid/views/products.py
Normal file
349
rattail/pyramid/views/products.py
Normal file
|
@ -0,0 +1,349 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2012 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||
# terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option)
|
||||
# any later version.
|
||||
#
|
||||
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||
# more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
|
||||
"""
|
||||
``rattail.pyramid.views.products`` -- Product Views
|
||||
"""
|
||||
|
||||
import threading
|
||||
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from webhelpers.html.tags import link_to
|
||||
|
||||
from pyramid.httpexceptions import HTTPFound
|
||||
from pyramid.renderers import render_to_response
|
||||
|
||||
import edbob
|
||||
from edbob.pyramid import Session
|
||||
from edbob.pyramid.progress import SessionProgress
|
||||
from edbob.pyramid.views import SearchableAlchemyGridView, CrudView
|
||||
|
||||
import rattail
|
||||
import rattail.labels
|
||||
from rattail import sil
|
||||
from rattail import batches
|
||||
from rattail.exceptions import LabelPrintingError
|
||||
from rattail.pyramid.forms import GPCFieldRenderer, PriceFieldRenderer
|
||||
|
||||
|
||||
class ProductsGrid(SearchableAlchemyGridView):
|
||||
|
||||
mapped_class = rattail.Product
|
||||
config_prefix = 'products'
|
||||
sort = 'description'
|
||||
|
||||
def join_map(self):
|
||||
|
||||
def join_vendor(q):
|
||||
q = q.outerjoin(
|
||||
rattail.ProductCost,
|
||||
and_(
|
||||
rattail.ProductCost.product_uuid == rattail.Product.uuid,
|
||||
rattail.ProductCost.preference == 1,
|
||||
))
|
||||
q = q.outerjoin(rattail.Vendor)
|
||||
return q
|
||||
|
||||
return {
|
||||
'brand':
|
||||
lambda q: q.outerjoin(rattail.Brand),
|
||||
'department':
|
||||
lambda q: q.outerjoin(rattail.Department,
|
||||
rattail.Department.uuid == rattail.Product.department_uuid),
|
||||
'subdepartment':
|
||||
lambda q: q.outerjoin(rattail.Subdepartment,
|
||||
rattail.Subdepartment.uuid == rattail.Product.subdepartment_uuid),
|
||||
'regular_price':
|
||||
lambda q: q.outerjoin(rattail.ProductPrice,
|
||||
rattail.ProductPrice.uuid == rattail.Product.regular_price_uuid),
|
||||
'current_price':
|
||||
lambda q: q.outerjoin(rattail.ProductPrice,
|
||||
rattail.ProductPrice.uuid == rattail.Product.current_price_uuid),
|
||||
'vendor':
|
||||
join_vendor,
|
||||
}
|
||||
|
||||
def filter_map(self):
|
||||
|
||||
def filter_upc():
|
||||
|
||||
def filter_is(q, v):
|
||||
try:
|
||||
v = int(v)
|
||||
except ValueError:
|
||||
return q
|
||||
else:
|
||||
return q.filter(rattail.Product.upc == v) if v else q
|
||||
|
||||
def filter_not(q, v):
|
||||
try:
|
||||
v = int(v)
|
||||
except ValueError:
|
||||
return q
|
||||
else:
|
||||
return q.filter(rattail.Product.upc != v) if v else q
|
||||
|
||||
return {'is': filter_is, 'nt': filter_not}
|
||||
|
||||
return self.make_filter_map(
|
||||
ilike=['description', 'size'],
|
||||
upc=filter_upc(),
|
||||
brand=self.filter_ilike(rattail.Brand.name),
|
||||
department=self.filter_ilike(rattail.Department.name),
|
||||
subdepartment=self.filter_ilike(rattail.Subdepartment.name),
|
||||
vendor=self.filter_ilike(rattail.Vendor.name))
|
||||
|
||||
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,
|
||||
filter_type_description='lk',
|
||||
include_filter_department=True,
|
||||
filter_type_department='lk',
|
||||
include_filter_vendor=True,
|
||||
filter_type_vendor='lk')
|
||||
|
||||
def sort_map(self):
|
||||
return self.make_sort_map(
|
||||
'upc', 'description', 'size',
|
||||
brand=self.sorter(rattail.Brand.name),
|
||||
department=self.sorter(rattail.Department.name),
|
||||
subdepartment=self.sorter(rattail.Subdepartment.name),
|
||||
regular_price=self.sorter(rattail.ProductPrice.price),
|
||||
current_price=self.sorter(rattail.ProductPrice.price),
|
||||
vendor=self.sorter(rattail.Vendor.name))
|
||||
|
||||
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.regular_price))
|
||||
q = q.options(joinedload(rattail.Product.current_price))
|
||||
q = q.options(joinedload(rattail.Product.vendor))
|
||||
return q
|
||||
|
||||
def grid(self):
|
||||
g = self.make_grid()
|
||||
g.upc.set(renderer=GPCFieldRenderer)
|
||||
g.regular_price.set(renderer=PriceFieldRenderer)
|
||||
g.current_price.set(renderer=PriceFieldRenderer)
|
||||
g.configure(
|
||||
include=[
|
||||
g.upc.label("UPC"),
|
||||
g.brand,
|
||||
g.description,
|
||||
g.size,
|
||||
g.subdepartment,
|
||||
g.vendor,
|
||||
g.regular_price.label("Reg. Price"),
|
||||
g.current_price.label("Cur. Price"),
|
||||
],
|
||||
readonly=True)
|
||||
|
||||
if self.request.has_perm('products.read'):
|
||||
g.clickable = True
|
||||
g.click_route_name = 'product.read'
|
||||
if self.request.has_perm('products.update'):
|
||||
g.editable = True
|
||||
g.edit_route_name = 'product.update'
|
||||
if self.request.has_perm('products.delete'):
|
||||
g.deletable = True
|
||||
g.delete_route_name = 'product.delete'
|
||||
|
||||
q = Session.query(rattail.LabelProfile)
|
||||
if q.count():
|
||||
def labels(row):
|
||||
return link_to("Print", '#', class_='print-label')
|
||||
g.add_column('labels', "Labels", labels)
|
||||
|
||||
return g
|
||||
|
||||
def render_kwargs(self):
|
||||
q = Session.query(rattail.LabelProfile)
|
||||
q = q.filter(rattail.LabelProfile.visible == True)
|
||||
q = q.order_by(rattail.LabelProfile.ordinal)
|
||||
return {'label_profiles': q.all()}
|
||||
|
||||
|
||||
class ProductCrud(CrudView):
|
||||
|
||||
mapped_class = rattail.Product
|
||||
home_route = 'products'
|
||||
|
||||
def fieldset(self, model):
|
||||
fs = self.make_fieldset(model)
|
||||
fs.upc.set(renderer=GPCFieldRenderer)
|
||||
fs.regular_price.set(renderer=PriceFieldRenderer)
|
||||
fs.current_price.set(renderer=PriceFieldRenderer)
|
||||
fs.configure(
|
||||
include=[
|
||||
fs.upc.label("UPC"),
|
||||
fs.brand,
|
||||
fs.description,
|
||||
fs.size,
|
||||
fs.department,
|
||||
fs.subdepartment,
|
||||
fs.regular_price,
|
||||
fs.current_price,
|
||||
])
|
||||
# if not self.readonly:
|
||||
# del fs.regular_price
|
||||
# del fs.current_price
|
||||
return fs
|
||||
|
||||
|
||||
def print_labels(request):
|
||||
profile = request.params.get('profile')
|
||||
profile = Session.query(rattail.LabelProfile).get(profile) if profile else None
|
||||
if not profile:
|
||||
return {'error': "Label profile not found"}
|
||||
|
||||
product = request.params.get('product')
|
||||
product = Session.query(rattail.Product).get(product) if product else None
|
||||
if not product:
|
||||
return {'error': "Product not found"}
|
||||
|
||||
quantity = request.params.get('quantity')
|
||||
if not quantity.isdigit():
|
||||
return {'error': "Quantity must be numeric"}
|
||||
quantity = int(quantity)
|
||||
|
||||
printer = profile.get_printer()
|
||||
if not printer:
|
||||
return {'error': "Couldn't get printer from label profile"}
|
||||
|
||||
try:
|
||||
printer.print_labels([(product, quantity)])
|
||||
except Exception, error:
|
||||
return {'error': str(error)}
|
||||
return {}
|
||||
|
||||
|
||||
class CreateProductsBatch(ProductsGrid):
|
||||
|
||||
def make_batch(self, provider, progress):
|
||||
session = edbob.Session()
|
||||
|
||||
self._filter_config = self.filter_config()
|
||||
self._sort_config = self.sort_config()
|
||||
products = self.make_query(session)
|
||||
|
||||
batch = provider.make_batch(session, products, progress)
|
||||
if not batch:
|
||||
session.rollback()
|
||||
session.close()
|
||||
return
|
||||
|
||||
session.commit()
|
||||
session.refresh(batch)
|
||||
session.close()
|
||||
|
||||
progress.session.load()
|
||||
progress.session['complete'] = True
|
||||
progress.session['success_url'] = self.request.route_url('batch.read', uuid=batch.uuid)
|
||||
progress.session['success_msg'] = "Batch \"%s\" has been created." % batch.description
|
||||
progress.session.save()
|
||||
|
||||
def __call__(self):
|
||||
if self.request.POST:
|
||||
provider = self.request.POST.get('provider')
|
||||
if provider:
|
||||
provider = batches.get_provider(provider)
|
||||
if provider:
|
||||
|
||||
if self.request.POST.get('params') == 'True':
|
||||
provider.set_params(Session(), **self.request.POST)
|
||||
|
||||
else:
|
||||
try:
|
||||
url = self.request.route_url('batch_params.%s' % provider.name)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self.request.session['referer'] = self.request.current_route_url()
|
||||
return HTTPFound(location=url)
|
||||
|
||||
progress = SessionProgress(self.request.session, 'products.batch')
|
||||
thread = threading.Thread(target=self.make_batch, args=(provider, progress))
|
||||
thread.start()
|
||||
kwargs = {
|
||||
'key': 'products.batch',
|
||||
'cancel_url': self.request.route_url('products'),
|
||||
'cancel_msg': "Batch creation was canceled.",
|
||||
}
|
||||
return render_to_response('/progress.mako', kwargs, request=self.request)
|
||||
|
||||
enabled = edbob.config.get('rattail.pyramid', 'batches.providers')
|
||||
if enabled:
|
||||
enabled = enabled.split()
|
||||
|
||||
providers = []
|
||||
for provider in batches.iter_providers():
|
||||
if not enabled or provider.name in enabled:
|
||||
providers.append((provider.name, provider.description))
|
||||
|
||||
return {'providers': providers}
|
||||
|
||||
|
||||
def includeme(config):
|
||||
|
||||
config.add_route('products', '/products')
|
||||
config.add_view(ProductsGrid, route_name='products',
|
||||
renderer='/products/index.mako',
|
||||
permission='products.list')
|
||||
|
||||
config.add_route('products.print_labels', '/products/labels')
|
||||
config.add_view(print_labels, route_name='products.print_labels',
|
||||
renderer='json', permission='products.print_labels')
|
||||
|
||||
config.add_route('products.create_batch', '/products/batch')
|
||||
config.add_view(CreateProductsBatch, route_name='products.create_batch',
|
||||
renderer='/products/batch.mako',
|
||||
permission='batches.create')
|
||||
|
||||
config.add_route('product.create', '/products/new')
|
||||
config.add_view(ProductCrud, attr='create', route_name='product.create',
|
||||
renderer='/products/crud.mako',
|
||||
permission='products.create')
|
||||
|
||||
config.add_route('product.read', '/products/{uuid}')
|
||||
config.add_view(ProductCrud, attr='read', route_name='product.read',
|
||||
renderer='/products/read.mako',
|
||||
permission='products.read')
|
||||
|
||||
config.add_route('product.update', '/products/{uuid}/edit')
|
||||
config.add_view(ProductCrud, attr='update', route_name='product.update',
|
||||
renderer='/products/crud.mako',
|
||||
permission='products.update')
|
||||
|
||||
config.add_route('product.delete', '/products/{uuid}/delete')
|
||||
config.add_view(ProductCrud, attr='delete', route_name='product.delete',
|
||||
permission='products.delete')
|
94
rattail/pyramid/views/reports.py
Normal file
94
rattail/pyramid/views/reports.py
Normal file
|
@ -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')
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue