diff --git a/tailbone/forms/renderers/__init__.py b/tailbone/forms/renderers/__init__.py index 2c6cd0f4..12c467a0 100644 --- a/tailbone/forms/renderers/__init__.py +++ b/tailbone/forms/renderers/__init__.py @@ -43,8 +43,8 @@ from .users import UserFieldRenderer, PermissionsFieldRenderer from .employees import EmployeeFieldRenderer -from .products import (ProductFieldRenderer, GPCFieldRenderer, BrandFieldRenderer, - PriceFieldRenderer, PriceWithExpirationFieldRenderer) +from .products import (GPCFieldRenderer, DepartmentFieldRenderer, BrandFieldRenderer, + ProductFieldRenderer, PriceFieldRenderer, PriceWithExpirationFieldRenderer) from .stores import StoreFieldRenderer diff --git a/tailbone/forms/renderers/products.py b/tailbone/forms/renderers/products.py index d3438119..984942d5 100644 --- a/tailbone/forms/renderers/products.py +++ b/tailbone/forms/renderers/products.py @@ -83,11 +83,16 @@ class DepartmentFieldRenderer(SelectFieldRenderer): """ Shows the department number as well as the name. """ + def render_readonly(self, **kwargs): - dept = self.raw_value - if dept: - return "{0} - {1}".format(dept.number, dept.name) - return "" + department = self.raw_value + if not department: + return '' + if department.number: + text = '{} {}'.format(department.number, department.name) + else: + text = department.name + return tags.link_to(text, self.request.route_url('departments.view', uuid=department.uuid)) class SubdepartmentFieldRenderer(SelectFieldRenderer): diff --git a/tailbone/templates/purchases/batches/create.mako b/tailbone/templates/purchases/batches/create.mako index 1a9ca0c4..b3bc0954 100644 --- a/tailbone/templates/purchases/batches/create.mako +++ b/tailbone/templates/purchases/batches/create.mako @@ -9,6 +9,7 @@ if (mode == ${enum.PURCHASE_BATCH_MODE_NEW}) { $('.field-wrapper.store_uuid').show(); $('.field-wrapper.purchase_uuid').hide(); + $('.field-wrapper.department_uuid').show(); $('.field-wrapper.buyer_uuid').show(); $('.field-wrapper.date_ordered').show(); $('.field-wrapper.date_received').hide(); @@ -17,6 +18,7 @@ } else if (mode == ${enum.PURCHASE_BATCH_MODE_RECEIVING}) { $('.field-wrapper.store_uuid').hide(); $('.field-wrapper.purchase_uuid').show(); + $('.field-wrapper.department_uuid').hide(); $('.field-wrapper.buyer_uuid').hide(); $('.field-wrapper.date_ordered').hide(); $('.field-wrapper.date_received').show(); @@ -25,6 +27,7 @@ } else if (mode == ${enum.PURCHASE_BATCH_MODE_COSTING}) { $('.field-wrapper.store_uuid').hide(); $('.field-wrapper.purchase_uuid').show(); + $('.field-wrapper.department_uuid').hide(); $('.field-wrapper.buyer_uuid').hide(); $('.field-wrapper.date_ordered').hide(); $('.field-wrapper.date_received').hide(); @@ -76,6 +79,7 @@ }); $('.field-wrapper.purchase_uuid select').selectmenu(); + $('.field-wrapper.department_uuid select').selectmenu(); show_mode(${form.fieldset.model.mode or enum.PURCHASE_BATCH_MODE_NEW}); diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 6a8a2d9d..a3af3e29 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -526,7 +526,12 @@ class MasterView(View): Return a "humanized" (and plural) version of the model name, for display in templates. """ - return getattr(cls, 'model_title_plural', '{0}s'.format(cls.get_model_title())) + if hasattr(cls, 'model_title_plural'): + return cls.model_title_plural + try: + return cls.get_model_class().get_model_title_plural() + except (NotImplementedError, AttributeError): + return '{}s'.format(cls.get_model_title()) @classmethod def get_route_prefix(cls): diff --git a/tailbone/views/purchases/batch.py b/tailbone/views/purchases/batch.py index eed1a745..bb73d894 100644 --- a/tailbone/views/purchases/batch.py +++ b/tailbone/views/purchases/batch.py @@ -31,7 +31,7 @@ import logging from sqlalchemy import orm -from rattail import enum, pod +from rattail import pod from rattail.db import model, api from rattail.gpc import GPC from rattail.time import localtime @@ -67,7 +67,6 @@ class PurchaseBatchView(BatchMasterView): Master view for purchase order batches. """ model_class = model.PurchaseBatch - model_title_plural = "Purchase Batches" model_row_class = model.PurchaseBatchRow default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler' route_prefix = 'purchases.batch' @@ -87,6 +86,10 @@ class PurchaseBatchView(BatchMasterView): default_active=True, default_verb='contains') g.sorters['vendor'] = g.make_sorter(model.Vendor.name) + g.joiners['department'] = lambda q: q.join(model.Department) + g.filters['department'] = g.make_filter('department', model.Department.name) + g.sorters['department'] = g.make_sorter(model.Department.name) + g.joiners['buyer'] = lambda q: q.join(model.Employee).join(model.Person) g.filters['buyer'] = g.make_filter('buyer', model.Person.display_name, default_active=True, default_verb='contains') @@ -106,6 +109,7 @@ class PurchaseBatchView(BatchMasterView): g.id, g.mode, g.vendor, + g.department, g.buyer, g.date_ordered, g.created, @@ -121,6 +125,8 @@ class PurchaseBatchView(BatchMasterView): fs.vendor.set(renderer=forms.renderers.VendorFieldRenderer, attrs={'selected': 'vendor_selected', 'cleared': 'vendor_cleared'}) + fs.department.set(renderer=forms.renderers.DepartmentFieldRenderer, + options=self.get_department_options()) fs.buyer.set(renderer=forms.renderers.EmployeeFieldRenderer) fs.po_number.set(label="PO Number") fs.po_total.set(label="PO Total", readonly=True, renderer=forms.renderers.CurrencyFieldRenderer) @@ -135,6 +141,10 @@ class PurchaseBatchView(BatchMasterView): fs.append(fa.Field('vendor_phone', readonly=True, value=self.get_vendor_phone_number)) + def get_department_options(self): + departments = Session.query(model.Department).order_by(model.Department.number) + return [('{} {}'.format(d.number, d.name), d.uuid) for d in departments] + def get_vendor_phone_number(self, batch): for phone in batch.vendor.phones: if phone.type == 'Voice': @@ -152,6 +162,7 @@ class PurchaseBatchView(BatchMasterView): fs.mode, fs.store, fs.vendor, + fs.department, fs.purchase, fs.vendor_email, fs.vendor_fax, @@ -208,6 +219,7 @@ class PurchaseBatchView(BatchMasterView): fs.mode.set(readonly=True) fs.store.set(readonly=True) fs.vendor.set(readonly=True) + fs.department.set(readonly=True) fs.purchase.set(readonly=True) def eligible_purchases(self): @@ -223,10 +235,10 @@ class PurchaseBatchView(BatchMasterView): purchases = Session.query(model.Purchase)\ .filter(model.Purchase.vendor == vendor) - if mode == enum.PURCHASE_BATCH_MODE_RECEIVING: + if mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING: purchases = purchases.filter(model.Purchase.status == self.enum.PURCHASE_STATUS_ORDERED)\ .order_by(model.Purchase.date_ordered, model.Purchase.created) - elif mode == enum.PURCHASE_BATCH_MODE_COSTING: + elif mode == self.enum.PURCHASE_BATCH_MODE_COSTING: purchases = purchases.filter(model.Purchase.status == self.enum.PURCHASE_STATUS_RECEIVED)\ .order_by(model.Purchase.date_received, model.Purchase.created) @@ -240,7 +252,7 @@ class PurchaseBatchView(BatchMasterView): elif purchase.status == self.enum.PURCHASE_STATUS_RECEIVED: date = purchase.date_received total = purchase.invoice_total - return '{} for ${:0,.2f} ({})'.format(date, total, purchase.buyer) + return '{} for ${:0,.2f} ({})'.format(date, total, purchase.department or purchase.buyer) def get_batch_kwargs(self, batch): kwargs = super(PurchaseBatchView, self).get_batch_kwargs(batch) @@ -253,6 +265,10 @@ class PurchaseBatchView(BatchMasterView): kwargs['vendor'] = batch.vendor elif batch.vendor_uuid: kwargs['vendor_uuid'] = batch.vendor_uuid + if batch.department: + kwargs['department'] = batch.department + elif batch.department_uuid: + kwargs['department_uuid'] = batch.department_uuid if batch.buyer: kwargs['buyer'] = batch.buyer elif batch.buyer_uuid: @@ -274,6 +290,8 @@ class PurchaseBatchView(BatchMasterView): purchase = Session.query(model.Purchase).get(batch.purchase_uuid) assert purchase kwargs['purchase'] = purchase + kwargs['buyer'] = purchase.buyer + kwargs['buyer_uuid'] = purchase.buyer_uuid kwargs['date_ordered'] = purchase.date_ordered kwargs['po_total'] = purchase.po_total @@ -510,7 +528,7 @@ class PurchaseBatchView(BatchMasterView): history = OrderedDict() purchases = Session.query(model.Purchase)\ .filter(model.Purchase.vendor == batch.vendor)\ - .filter(model.Purchase.status >= enum.PURCHASE_STATUS_ORDERED)\ + .filter(model.Purchase.status >= self.enum.PURCHASE_STATUS_ORDERED)\ .order_by(model.Purchase.date_ordered.desc(), model.Purchase.created.desc())\ .options(orm.joinedload(model.Purchase.items))[:6] for purchase in purchases[:6]: diff --git a/tailbone/views/purchases/core.py b/tailbone/views/purchases/core.py index b1c62f51..df5e86b1 100644 --- a/tailbone/views/purchases/core.py +++ b/tailbone/views/purchases/core.py @@ -100,6 +100,10 @@ class PurchaseView(MasterView): default_active=True, default_verb='contains') g.sorters['vendor'] = g.make_sorter(model.Vendor.name) + g.joiners['department'] = lambda q: q.join(model.Department) + g.filters['department'] = g.make_filter('department', model.Department.name) + g.sorters['department'] = g.make_sorter(model.Department.name) + g.joiners['buyer'] = lambda q: q.join(model.Employee).join(model.Person) g.filters['buyer'] = g.make_filter('buyer', model.Person.display_name, default_active=True, default_verb='contains') @@ -121,6 +125,7 @@ class PurchaseView(MasterView): include=[ g.store, g.vendor, + g.department, g.buyer, g.date_ordered, g.date_received, @@ -130,6 +135,7 @@ class PurchaseView(MasterView): def _preconfigure_fieldset(self, fs): fs.vendor.set(renderer=forms.renderers.VendorFieldRenderer) + fs.department.set(renderer=forms.renderers.DepartmentFieldRenderer) fs.status.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.PURCHASE_STATUS), readonly=True) fs.po_number.set(label="PO Number") @@ -142,6 +148,7 @@ class PurchaseView(MasterView): include=[ fs.store, fs.vendor, + fs.department, fs.status, fs.buyer, fs.date_ordered, @@ -162,6 +169,16 @@ class PurchaseView(MasterView): del fs.invoice_number del fs.invoice_total + def delete_instance(self, purchase): + """ + Delete all batches for the purchase, then delete the purchase. + """ + for batch in list(purchase.batches): + self.Session.delete(batch) + self.Session.flush() + self.Session.delete(purchase) + self.Session.flush() + def get_parent(self, item): return item.purchase