From 67f6c11307ab8659f92775abe16612368c74be73 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 21 Nov 2016 01:07:35 -0600 Subject: [PATCH] Add support for 'receiving' mode for purchase batches --- tailbone/forms/renderers/products.py | 6 + tailbone/forms/renderers/vendors.py | 4 +- tailbone/subscribers.py | 3 +- .../templates/purchases/batches/create.mako | 62 ++++++- .../purchases/batches/order_form.mako | 25 ++- .../templates/purchases/batches/view.mako | 2 +- tailbone/views/batch.py | 26 +-- tailbone/views/purchases/batch.py | 175 ++++++++++++++---- tailbone/views/purchases/core.py | 59 +++++- 9 files changed, 292 insertions(+), 70 deletions(-) diff --git a/tailbone/forms/renderers/products.py b/tailbone/forms/renderers/products.py index 0377fb06..c3c82f52 100644 --- a/tailbone/forms/renderers/products.py +++ b/tailbone/forms/renderers/products.py @@ -49,6 +49,12 @@ class ProductFieldRenderer(AutocompleteFieldRenderer): return product.full_description return '' + def render_readonly(self, **kwargs): + product = self.raw_value + if not product: + return '' + return tags.link_to(product, self.request.route_url('products.view', uuid=product.uuid)) + class GPCFieldRenderer(TextFieldRenderer): """ diff --git a/tailbone/forms/renderers/vendors.py b/tailbone/forms/renderers/vendors.py index b0ce30e8..5647841a 100644 --- a/tailbone/forms/renderers/vendors.py +++ b/tailbone/forms/renderers/vendors.py @@ -26,7 +26,7 @@ Vendor Field Renderers from __future__ import unicode_literals, absolute_import -import formalchemy as fa +from formalchemy.fields import SelectFieldRenderer from webhelpers.html import tags from tailbone.forms.renderers.common import AutocompleteFieldRenderer @@ -45,7 +45,7 @@ class VendorFieldRenderer(AutocompleteFieldRenderer): return tags.link_to(vendor, self.request.route_url('vendors.view', uuid=vendor.uuid)) -class PurchaseFieldRenderer(fa.FieldRenderer): +class PurchaseFieldRenderer(SelectFieldRenderer): """ Renderer for :class:`rattail.db.model.Purchase` relation fields. """ diff --git a/tailbone/subscribers.py b/tailbone/subscribers.py index 7d3adc67..c33b8a10 100644 --- a/tailbone/subscribers.py +++ b/tailbone/subscribers.py @@ -27,7 +27,6 @@ Event Subscribers from __future__ import unicode_literals, absolute_import import rattail -from rattail import enum from rattail.db import model from rattail.db.auth import has_permission, administrator_role @@ -79,6 +78,7 @@ def before_render(event): renderer_globals['url'] = request.route_url renderer_globals['rattail'] = rattail renderer_globals['tailbone'] = tailbone + renderer_globals['enum'] = request.rattail_config.get_enum() def add_inbox_count(event): @@ -92,6 +92,7 @@ def add_inbox_count(event): request = event.get('request') or threadlocal.get_current_request() if request.user: renderer_globals = event + enum = request.rattail_config.get_enum() renderer_globals['inbox_count'] = Session.query(model.Message)\ .outerjoin(model.MessageRecipient)\ .filter(model.MessageRecipient.recipient == Session.merge(request.user))\ diff --git a/tailbone/templates/purchases/batches/create.mako b/tailbone/templates/purchases/batches/create.mako index 22a206d0..11c1a320 100644 --- a/tailbone/templates/purchases/batches/create.mako +++ b/tailbone/templates/purchases/batches/create.mako @@ -4,11 +4,71 @@ <%def name="head_tags()"> ${parent.head_tags()} diff --git a/tailbone/templates/purchases/batches/order_form.mako b/tailbone/templates/purchases/batches/order_form.mako index 8ca58692..7060b980 100644 --- a/tailbone/templates/purchases/batches/order_form.mako +++ b/tailbone/templates/purchases/batches/order_form.mako @@ -154,7 +154,18 @@ Pref. Unit Cost % for data in history.itervalues(): - ${data['purchase'].date_ordered.strftime('%m/%d') if data else ''} + + % if data: + % if data['purchase'].date_ordered: + ${data['purchase'].date_ordered.strftime('%m/%d') if data else ''} + % elif data['purchase'].date_received: + Rec.
+ ${data['purchase'].date_received.strftime('%m/%d') if data else ''} + % else: + ?? + % endif + % endif + % endfor ${batch.date_ordered.strftime('%m/%d')}
@@ -180,9 +191,15 @@ $${'{:0.2f}'.format(cost.unit_cost)} % for data in history.itervalues(): - % if data and cost.product_uuid in data['items']: - ${'{} / {}'.format(int(data['items'][cost.product_uuid].cases_ordered or 0), int(data['items'][cost.product_uuid].units_ordered or 0))} -## ${int(data['items'][cost.product_uuid].cases_ordered or 0) or ''} + % if data: + <% item = data['items'].get(cost.product_uuid) %> + % if item: + % if data['purchase'].date_ordered and (item.cases_ordered is not None or item.units_ordered is not None): + ${'{} / {}'.format(int(item.cases_ordered or 0), int(item.units_ordered or 0))} + % elif item.cases_received is not None or item.units_received is not None: + ${'{} / {}'.format(int(item.cases_received or 0), int(item.units_received or 0))} + % endif + % endif % endif % endfor diff --git a/tailbone/templates/purchases/batches/view.mako b/tailbone/templates/purchases/batches/view.mako index 1be34ce1..3115a3b1 100644 --- a/tailbone/templates/purchases/batches/view.mako +++ b/tailbone/templates/purchases/batches/view.mako @@ -23,7 +23,7 @@ <%def name="leading_buttons()"> - % if not batch.complete and not batch.executed and request.has_perm('purchases.batch.order_form'): + % if batch.mode == enum.PURCHASE_BATCH_MODE_NEW and not batch.complete and not batch.executed and request.has_perm('purchases.batch.order_form'): % endif diff --git a/tailbone/views/batch.py b/tailbone/views/batch.py index 3b7faa1c..01f113ac 100644 --- a/tailbone/views/batch.py +++ b/tailbone/views/batch.py @@ -237,19 +237,18 @@ class BatchMasterView(MasterView): def save_create_form(self, form): self.before_create(form) - # current user is batch creator - creator = self.request.user or self.late_login_user() - - # transfer form data to batch instance - form.fieldset.sync() - batch = form.fieldset.model - batch.created_by = creator - - # destroy initial batch and re-make using handler - kwargs = self.get_batch_kwargs(batch) - Session.expunge(batch) - # TODO: is no_autoflush necessary? with Session.no_autoflush: + + # transfer form data to batch instance + form.fieldset.sync() + batch = form.fieldset.model + + # current user is batch creator + batch.created_by = self.request.user or self.late_login_user() + + # destroy initial batch and re-make using handler + kwargs = self.get_batch_kwargs(batch) + Session.expunge(batch) batch = self.handler.make_batch(Session(), **kwargs) Session.flush() @@ -279,6 +278,7 @@ class BatchMasterView(MasterView): kwargs['filename'] = batch.filename return kwargs + # TODO: deprecate / remove this (is it used at all now?) def init_batch(self, batch): """ Initialize a new batch. Derived classes can override this to @@ -667,7 +667,7 @@ class BatchMasterView(MasterView): fs.status_text.set(readonly=True) fs.removed.set(readonly=True) try: - fs.product.set(readonly=True) + fs.product.set(readonly=True, renderer=forms.renderers.ProductFieldRenderer) except AttributeError: pass diff --git a/tailbone/views/purchases/batch.py b/tailbone/views/purchases/batch.py index 5132012b..51fe9847 100644 --- a/tailbone/views/purchases/batch.py +++ b/tailbone/views/purchases/batch.py @@ -36,6 +36,7 @@ from rattail.core import Object from rattail.util import OrderedDict import formalchemy as fa +from pyramid import httpexceptions from tailbone import forms from tailbone.db import Session @@ -56,6 +57,9 @@ class PurchaseBatchView(BatchMasterView): rows_editable = True edit_with_rows = False + def get_instance_title(self, batch): + return '{} ({})'.format(batch.id_str, self.enum.PURCHASE_BATCH_MODE[batch.mode]) + def _preconfigure_grid(self, g): super(PurchaseBatchView, self)._preconfigure_grid(g) @@ -74,6 +78,7 @@ class PurchaseBatchView(BatchMasterView): g.filters['complete'].default_verb = 'is_true' g.date_ordered.set(label="Ordered") + g.date_received.set(label="Received") g.mode.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.PURCHASE_BATCH_MODE)) def configure_grid(self, g): @@ -93,11 +98,14 @@ class PurchaseBatchView(BatchMasterView): def _preconfigure_fieldset(self, fs): super(PurchaseBatchView, self)._preconfigure_fieldset(fs) fs.mode.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.PURCHASE_BATCH_MODE)) - fs.purchase.set(renderer=forms.renderers.PurchaseFieldRenderer) - fs.vendor.set(renderer=forms.renderers.VendorFieldRenderer) + fs.purchase.set(renderer=forms.renderers.PurchaseFieldRenderer, options=[]) + fs.vendor.set(renderer=forms.renderers.VendorFieldRenderer, + attrs={'selected': 'vendor_selected', + 'cleared': 'vendor_cleared'}) fs.buyer.set(renderer=forms.renderers.EmployeeFieldRenderer) fs.po_number.set(label="PO Number") - fs.po_total.set(label="PO Total", readonly=True) + fs.po_total.set(label="PO Total", readonly=True, renderer=forms.renderers.CurrencyFieldRenderer) + fs.invoice_total.set(readonly=True, renderer=forms.renderers.CurrencyFieldRenderer) fs.append(fa.Field('vendor_email', readonly=True, value=lambda b: b.vendor.email.address if b.vendor.email else None)) @@ -123,17 +131,19 @@ class PurchaseBatchView(BatchMasterView): include=[ fs.id, fs.mode, - fs.purchase, fs.store, fs.vendor, + fs.purchase, fs.vendor_email, fs.vendor_fax, fs.vendor_contact, fs.vendor_phone, fs.buyer, fs.date_ordered, + fs.date_received, fs.po_number, fs.po_total, + fs.invoice_total, fs.created, fs.created_by, fs.complete, @@ -142,8 +152,8 @@ class PurchaseBatchView(BatchMasterView): ]) if self.creating: - del fs.purchase del fs.po_total + del fs.invoice_total del fs.complete del fs.vendor_email del fs.vendor_fax @@ -163,12 +173,14 @@ class PurchaseBatchView(BatchMasterView): if buyer: fs.model.buyer = buyer - # default order date is today - fs.model.date_ordered = localtime(self.rattail_config).date() + # TODO: something tells me this isn't quite safe.. + # all dates have today as default + today = localtime(self.rattail_config).date() + fs.model.date_ordered = today + fs.model.date_received = today # TODO: temp hack until we support more modes modes = dict(self.enum.PURCHASE_BATCH_MODE) - del modes[self.enum.PURCHASE_BATCH_MODE_RECEIVING] del modes[self.enum.PURCHASE_BATCH_MODE_COSTING] fs.mode.set(renderer=forms.renderers.EnumFieldRenderer(modes)) @@ -176,6 +188,30 @@ class PurchaseBatchView(BatchMasterView): fs.mode.set(readonly=True) fs.store.set(readonly=True) fs.vendor.set(readonly=True) + fs.purchase.set(readonly=True) + + def eligible_purchases(self): + uuid = self.request.GET.get('vendor_uuid') + vendor = Session.query(model.Vendor).get(uuid) if uuid else None + if not vendor: + return {'error': "Must specify a vendor."} + + mode = self.request.GET.get('mode') + mode = int(mode) if mode and mode.isdigit() else None + if not mode or mode not in self.enum.PURCHASE_BATCH_MODE: + return {'error': "Unknown mode: {}".format(mode)} + + purchases = Session.query(model.Purchase)\ + .filter(model.Purchase.vendor == vendor) + if mode == 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) + + return {'purchases': [{'uuid': p.uuid, 'display': self.render_eligible_purchase(p)} + for p in purchases]} + + def render_eligible_purchase(self, purchase): + return '{} for ${:0,.2f} ({})'.format(purchase.date_ordered, purchase.po_total, purchase.buyer) def get_batch_kwargs(self, batch): kwargs = super(PurchaseBatchView, self).get_batch_kwargs(batch) @@ -192,8 +228,20 @@ class PurchaseBatchView(BatchMasterView): kwargs['buyer'] = batch.buyer elif batch.buyer_uuid: kwargs['buyer_uuid'] = batch.buyer_uuid - kwargs['date_ordered'] = batch.date_ordered kwargs['po_number'] = batch.po_number + + if batch.mode == self.enum.PURCHASE_BATCH_MODE_NEW: + kwargs['date_ordered'] = batch.date_ordered + + elif batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING: + kwargs['date_received'] = batch.date_received + if batch.purchase_uuid: + purchase = Session.query(model.Purchase).get(batch.purchase_uuid) + assert purchase + kwargs['purchase'] = purchase + kwargs['date_ordered'] = purchase.date_ordered + kwargs['po_total'] = purchase.po_total + return kwargs def template_kwargs_view(self, **kwargs): @@ -214,11 +262,16 @@ class PurchaseBatchView(BatchMasterView): g.upc.set(label="UPC") g.brand_name.set(label="Brand") - g.cases_ordered.set(label="Cases") - g.units_ordered.set(label="Units") - g.po_total.set(label="Total") + g.cases_ordered.set(label="Cases Ord.") + g.units_ordered.set(label="Units Ord.") + g.cases_received.set(label="Cases Rec.") + g.units_received.set(label="Units Rec.") + g.po_total.set(label="Total", renderer=forms.renderers.CurrencyFieldRenderer) + g.invoice_total.set(label="Total", renderer=forms.renderers.CurrencyFieldRenderer) def configure_row_grid(self, g): + batch = self.get_instance() + g.configure( include=[ g.sequence, @@ -228,32 +281,38 @@ class PurchaseBatchView(BatchMasterView): g.size, g.cases_ordered, g.units_ordered, + g.cases_received, + g.units_received, g.po_total, + g.invoice_total, g.status_code, ], readonly=True) + if batch.mode == self.enum.PURCHASE_BATCH_MODE_NEW: + del g.cases_received + del g.units_received + del g.invoice_total + elif batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING: + del g.po_total + def make_row_grid_tools(self, batch): return self.make_default_row_grid_tools(batch) -# def row_grid_row_attrs(self, row, i): -# attrs = {} -# if row.status_code in (row.STATUS_NOT_IN_PURCHASE, -# row.STATUS_NOT_IN_INVOICE, -# row.STATUS_DIFFERS_FROM_PURCHASE): -# attrs['class_'] = 'notice' -# if row.status_code in (row.STATUS_NOT_IN_DB, -# row.STATUS_COST_NOT_IN_DB, -# row.STATUS_NO_CASE_QUANTITY): -# attrs['class_'] = 'warning' -# return attrs + def row_grid_row_attrs(self, row, i): + attrs = {} + if row.status_code in (row.STATUS_INCOMPLETE, + row.STATUS_ORDERED_RECEIVED_DIFFER): + attrs['class_'] = 'notice' + return attrs def _preconfigure_row_fieldset(self, fs): super(PurchaseBatchView, self)._preconfigure_row_fieldset(fs) fs.upc.set(label="UPC") fs.brand_name.set(label="Brand") fs.po_unit_cost.set(label="PO Unit Cost") - fs.po_total.set(label="PO Total") + fs.po_total.set(label="PO Total", renderer=forms.renderers.CurrencyFieldRenderer) + fs.invoice_total.set(renderer=forms.renderers.CurrencyFieldRenderer) fs.append(fa.Field('item_lookup', label="Item Lookup Code", required=True, validate=self.item_lookup)) @@ -277,35 +336,67 @@ class PurchaseBatchView(BatchMasterView): raise fa.ValidationError("Product not found") def configure_row_fieldset(self, fs): + try: + batch = self.get_instance() + except httpexceptions.HTTPNotFound: + batch = self.get_row_instance().batch + + fs.configure( + include=[ + fs.item_lookup, + fs.upc, + fs.product, + fs.cases_ordered, + fs.units_ordered, + fs.cases_received, + fs.units_received, + fs.po_total, + fs.invoice_total, + ]) if self.creating: - fs.configure( - include=[ - fs.item_lookup, - fs.cases_ordered, - fs.units_ordered, - ]) + del fs.upc + del fs.product + del fs.po_total + del fs.invoice_total + if batch.mode == self.enum.PURCHASE_BATCH_MODE_NEW: + del fs.cases_received + del fs.units_received + elif batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING: + del fs.cases_ordered + del fs.units_ordered elif self.editing: - fs.configure( - include=[ - fs.upc.readonly(), - fs.product.readonly(), - fs.cases_ordered, - fs.units_ordered, - ]) + del fs.item_lookup + fs.upc.set(readonly=True) + fs.product.set(readonly=True) + del fs.po_total + del fs.invoice_total + + elif self.viewing: + del fs.item_lookup def before_create_row(self, form): row = form.fieldset.model batch = self.get_instance() - row.sequence = max([0] + [r.sequence for r in batch.data_rows]) + 1 - row.batch = batch + batch.add_row(row) # TODO: this seems heavy-handed but works.. row.product_uuid = self.item_lookup(form.fieldset.item_lookup.value) def after_create_row(self, row): self.handler.refresh_row(row) + def after_edit_row(self, row): + batch = row.batch + + # first undo any totals previously in effect for the row + if batch.mode == self.enum.PURCHASE_BATCH_MODE_NEW and row.po_total: + batch.po_total -= row.po_total + elif batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING and row.invoice_total: + batch.invoice_total -= row.invoice_total + + self.handler.refresh_row(row) + def redirect_after_create_row(self, row): self.request.session.flash("Added item: {} {}".format(row.upc.pretty(), row.product)) return self.redirect(self.request.current_route_url()) @@ -478,6 +569,12 @@ class PurchaseBatchView(BatchMasterView): model_key = cls.get_model_key() model_title = cls.get_model_title() + # eligible purchases (AJAX) + config.add_route('{}.eligible_purchases'.format(route_prefix), '{}/eligible-purchases'.format(url_prefix)) + config.add_view(cls, attr='eligible_purchases', route_name='{}.eligible_purchases'.format(route_prefix), + renderer='json', permission='{}.view'.format(permission_prefix)) + + # defaults cls._batch_defaults(config) cls._defaults(config) diff --git a/tailbone/views/purchases/core.py b/tailbone/views/purchases/core.py index 4716f8a9..6acaff77 100644 --- a/tailbone/views/purchases/core.py +++ b/tailbone/views/purchases/core.py @@ -45,8 +45,9 @@ class BatchesFieldRenderer(fa.FieldRenderer): return '' def render(batch): - return tags.link_to('{} ({})'.format(batch.id_str, enum.PURCHASE_BATCH_MODE[batch.mode]), - self.request.route_url('purchases.batch.view', uuid=batch.uuid)) + display = '{} ({}){}'.format(batch.id_str, enum.PURCHASE_BATCH_MODE[batch.mode], + '' if batch.executed else ' (pending)') + return tags.link_to(display, self.request.route_url('purchases.batch.view', uuid=batch.uuid)) enum = self.request.rattail_config.get_enum() items = [HTML.tag('li', c=render(batch)) for batch in batches] @@ -65,6 +66,17 @@ class PurchaseView(MasterView): model_row_class = model.PurchaseItem row_model_title = 'Purchase Item' + def get_instance_title(self, purchase): + if purchase.status >= self.enum.PURCHASE_STATUS_RECEIVED: + if purchase.date_received: + return "{} (received {})".format(purchase.vendor, purchase.date_received.strftime('%Y-%m-%d')) + return "{} (received)".format(purchase.vendor) + elif purchase.status >= self.enum.PURCHASE_STATUS_ORDERED: + if purchase.date_ordered: + return "{} (ordered {})".format(purchase.vendor, purchase.date_ordered.strftime('%Y-%m-%d')) + return "{} (ordered)".format(purchase.vendor) + return unicode(purchase) + def _preconfigure_grid(self, g): g.joiners['store'] = lambda q: q.join(model.Store) g.filters['store'] = g.make_filter('store', model.Store.name) @@ -106,7 +118,8 @@ class PurchaseView(MasterView): fs.status.set(renderer=forms.renderers.EnumFieldRenderer(enum.PURCHASE_STATUS), readonly=True) fs.po_number.set(label="PO Number") - fs.po_total.set(label="PO Total") + fs.po_total.set(label="PO Total", renderer=forms.renderers.CurrencyFieldRenderer) + fs.invoice_total.set(renderer=forms.renderers.CurrencyFieldRenderer) fs.batches.set(renderer=BatchesFieldRenderer) def configure_fieldset(self, fs): @@ -114,15 +127,24 @@ class PurchaseView(MasterView): include=[ fs.store, fs.vendor, + fs.status, fs.buyer, fs.date_ordered, + fs.date_received, fs.po_number, fs.po_total, - fs.status, + fs.invoice_number, + fs.invoice_total, fs.created, fs.created_by, fs.batches, ]) + if self.viewing: + purchase = fs.model + if purchase.status == self.enum.PURCHASE_STATUS_ORDERED: + del fs.date_received + del fs.invoice_number + del fs.invoice_total def get_parent(self, item): return item.purchase @@ -136,11 +158,15 @@ class PurchaseView(MasterView): g.sequence.set(label="Seq") g.upc.set(label="UPC") g.brand_name.set(label="Brand") - g.cases_ordered.set(label="Cases") - g.units_ordered.set(label="Units") - g.po_total.set(label="PO Total") + g.cases_ordered.set(label="Cases Ord.") + g.units_ordered.set(label="Units Ord.") + g.cases_received.set(label="Cases Rec.") + g.units_received.set(label="Units Rec.") + g.po_total.set(label="Total", renderer=forms.renderers.CurrencyFieldRenderer) + g.invoice_total.set(label="Total", renderer=forms.renderers.CurrencyFieldRenderer) def configure_row_grid(self, g): + purchase = self.get_instance() g.configure( include=[ g.sequence, @@ -150,15 +176,26 @@ class PurchaseView(MasterView): g.size, g.cases_ordered, g.units_ordered, + g.cases_received, + g.units_received, g.po_total, + g.invoice_total, ], readonly=True) + if purchase.status == enum.PURCHASE_STATUS_ORDERED: + del g.cases_received + del g.units_received + del g.invoice_total + elif purchase.status == enum.PURCHASE_STATUS_RECEIVED: + del g.po_total def _preconfigure_row_fieldset(self, fs): fs.vendor_code.set(label="Vendor Item Code") fs.upc.set(label="UPC") - fs.po_unit_cost.set(label="PO Unit Cost") - fs.po_total.set(label="PO Total") + fs.po_unit_cost.set(label="PO Unit Cost", renderer=forms.renderers.CurrencyFieldRenderer) + fs.po_total.set(label="PO Total", renderer=forms.renderers.CurrencyFieldRenderer) + fs.invoice_unit_cost.set(renderer=forms.renderers.CurrencyFieldRenderer) + fs.invoice_total.set(renderer=forms.renderers.CurrencyFieldRenderer) fs.append(fa.Field('department', value=lambda i: '{} {}'.format(i.department_number, i.department_name))) def configure_row_fieldset(self, fs): @@ -173,8 +210,12 @@ class PurchaseView(MasterView): fs.case_quantity, fs.cases_ordered, fs.units_ordered, + fs.cases_received, + fs.units_received, fs.po_unit_cost, fs.po_total, + fs.invoice_unit_cost, + fs.invoice_total, ])