From 8137d715df1388977d9c280e71f0cb3db8dda27b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 5 Feb 2018 12:59:34 -0600 Subject: [PATCH] Refactor purchasing batch views per master4 --- .../templates/mobile/ordering/create_row.mako | 2 +- tailbone/templates/ordering/order_form.mako | 6 +- .../purchases/receiving_worksheet.mako | 4 +- tailbone/views/purchasing/batch.py | 417 ++++++++++++------ tailbone/views/purchasing/ordering.py | 46 +- tailbone/views/purchasing/receiving.py | 39 +- 6 files changed, 328 insertions(+), 186 deletions(-) diff --git a/tailbone/templates/mobile/ordering/create_row.mako b/tailbone/templates/mobile/ordering/create_row.mako index d31814f8..79d83630 100644 --- a/tailbone/templates/mobile/ordering/create_row.mako +++ b/tailbone/templates/mobile/ordering/create_row.mako @@ -1,6 +1,6 @@ ## -*- coding: utf-8; -*- <%inherit file="/mobile/master/create_row.mako" /> -<%def name="title()">${h.link_to(index_title, index_url)} » ${h.link_to(instance_title, instance_url)} » Add Item +<%def name="page_title()">${h.link_to(index_title, index_url)} » ${h.link_to(instance_title, instance_url)} » Add Item ${parent.body()} diff --git a/tailbone/templates/ordering/order_form.mako b/tailbone/templates/ordering/order_form.mako index bca0ff6f..d427af4c 100644 --- a/tailbone/templates/ordering/order_form.mako +++ b/tailbone/templates/ordering/order_form.mako @@ -288,5 +288,9 @@ ${h.end_form()} ${h.pretty_quantity(cost.case_size)} ${"LB" if cost.product.weighed else "EA"} ${cost.code or ''} ${'X' if cost.preference == 1 else ''} - $${'{:0.2f}'.format(cost.unit_cost)} + + % if cost.unit_cost is not None: + $${'{:0.2f}'.format(cost.unit_cost)} + % endif + diff --git a/tailbone/templates/purchases/receiving_worksheet.mako b/tailbone/templates/purchases/receiving_worksheet.mako index 38696dd4..596bfd43 100644 --- a/tailbone/templates/purchases/receiving_worksheet.mako +++ b/tailbone/templates/purchases/receiving_worksheet.mako @@ -1,4 +1,4 @@ -## -*- coding: utf-8 -*- +## -*- coding: utf-8; -*- Receiving Worksheet @@ -76,7 +76,7 @@ % for item in purchase.items: - ${item.upc.pretty()} + ${item.upc.pretty() if item.upc else item.item_id} ${item.vendor_code or ''} ${(item.brand_name or '')[:15]} ${item.description or ''} diff --git a/tailbone/views/purchasing/batch.py b/tailbone/views/purchasing/batch.py index c7311f40..6a41cdca 100644 --- a/tailbone/views/purchasing/batch.py +++ b/tailbone/views/purchasing/batch.py @@ -36,8 +36,9 @@ from deform import widget as dfwidget from pyramid import httpexceptions from webhelpers2.html import tags -from tailbone import forms, forms2 -from tailbone.views.batch import BatchMasterView3 as BatchMasterView +# from tailbone import forms +from tailbone import forms2 +from tailbone.views.batch import BatchMasterView4 as BatchMasterView class PurchasingBatchView(BatchMasterView): @@ -56,27 +57,11 @@ class PurchasingBatchView(BatchMasterView): 'date_ordered', 'created', 'created_by', + 'rowcount', 'status_code', 'executed', ] - # row_grid_columns = [ - # 'sequence', - # 'upc', - # # 'item_id', - # 'brand_name', - # 'description', - # 'size', - # 'cases_ordered', - # 'units_ordered', - # 'cases_received', - # 'units_received', - # 'po_total', - # 'invoice_total', - # 'credits', - # 'status_code', - # ] - form_fields = [ 'id', 'store', @@ -104,6 +89,88 @@ class PurchasingBatchView(BatchMasterView): 'executed_by', ] + row_labels = { + 'upc': "UPC", + 'item_id': "Item ID", + 'brand_name': "Brand", + 'po_line_number': "PO Line Number", + 'po_unit_cost': "PO Unit Cost", + 'po_total': "PO Total", + } + + # row_grid_columns = [ + # 'sequence', + # 'upc', + # # 'item_id', + # 'brand_name', + # 'description', + # 'size', + # 'cases_ordered', + # 'units_ordered', + # 'cases_received', + # 'units_received', + # 'po_total', + # 'invoice_total', + # 'credits', + # 'status_code', + # ] + + row_form_fields = [ + 'upc', + 'item_id', + 'product', + 'brand_name', + 'description', + 'size', + 'case_quantity', + 'cases_ordered', + 'units_ordered', + 'cases_received', + 'units_received', + 'cases_damaged', + 'units_damaged', + 'cases_expired', + 'units_expired', + 'cases_mispick', + 'units_mispick', + 'po_line_number', + 'po_unit_cost', + 'po_total', + 'invoice_line_number', + 'invoice_unit_cost', + 'invoice_total', + 'status_code', + 'credits', + ] + + mobile_row_form_fields = [ + 'upc', + 'item_id', + 'product', + 'brand_name', + 'description', + 'size', + 'case_quantity', + 'cases_ordered', + 'units_ordered', + 'cases_received', + 'units_received', + 'cases_damaged', + 'units_damaged', + 'cases_expired', + 'units_expired', + 'cases_mispick', + 'units_mispick', + # 'po_line_number', + 'po_unit_cost', + 'po_total', + # 'invoice_line_number', + 'invoice_unit_cost', + 'invoice_total', + 'status_code', + # 'credits', + ] + @property def batch_mode(self): raise NotImplementedError("Please define `batch_mode` for your purchasing batch view") @@ -156,7 +223,11 @@ class PurchasingBatchView(BatchMasterView): # TODO: this hardly seems complete... # store - if not self.creating: + if self.creating: + f.replace('store', 'store_uuid') + f.set_widget('store_uuid', dfwidget.SelectWidget(values=self.get_store_values())) + f.set_label('store_uuid', "Store") + else: f.set_readonly('store') f.set_renderer('store', self.render_store) @@ -189,12 +260,13 @@ class PurchasingBatchView(BatchMasterView): # department f.set_renderer('department', self.render_department) if self.creating: - f.replace('department', 'department_uuid') - f.set_node('department_uuid', colander.String()) - dept_options = self.get_department_options() - dept_values = [(v, k) for k, v in dept_options] - f.set_widget('department_uuid', dfwidget.SelectWidget(values=dept_values)) - f.set_label('department_uuid', "Department") + if 'department' in f.fields: + f.replace('department', 'department_uuid') + f.set_node('department_uuid', colander.String()) + dept_options = self.get_department_options() + dept_values = [(v, k) for k, v in dept_options] + f.set_widget('department_uuid', dfwidget.SelectWidget(values=dept_values)) + f.set_label('department_uuid', "Department") else: f.set_readonly('department') @@ -271,6 +343,12 @@ class PurchasingBatchView(BatchMasterView): 'vendor_contact', 'status_code') + def configure_mobile_form(self, f): + super(PurchasingBatchView, self).configure_mobile_form(f) + + # currency fields + f.set_type('po_total', 'currency') + def render_store(self, batch, field): store = batch.store if not store: @@ -330,6 +408,37 @@ class PurchasingBatchView(BatchMasterView): return tags.link_to(text, url) return text + def get_store_values(self): + stores = self.Session.query(model.Store)\ + .order_by(model.Store.id) + return [(s.uuid, "({}) {}".format(s.id, s.name)) + for s in stores] + + def get_vendors(self): + return self.Session.query(model.Vendor)\ + .order_by(model.Vendor.name) + + def get_vendor_values(self): + vendors = self.get_vendors() + return [(v.uuid, "({}) {}".format(v.id, v.name)) + for v in vendors] + + def get_vendor_values(self): + vendors = self.get_vendors() + return [(v.uuid, "({}) {}".format(v.id, v.name)) + for v in vendors] + + def get_buyers(self): + return self.Session.query(model.Employee)\ + .join(model.Person)\ + .filter(model.Employee.status == self.enum.EMPLOYEE_STATUS_CURRENT)\ + .order_by(model.Person.display_name) + + def get_buyer_values(self): + buyers = self.get_buyers() + return [(b.uuid, six.text_type(b)) + for b in buyers] + def get_department_options(self): departments = self.Session.query(model.Department).order_by(model.Department.number) return [('{} {}'.format(d.number, d.name), d.uuid) for d in departments] @@ -456,8 +565,6 @@ class PurchasingBatchView(BatchMasterView): g.set_type('invoice_total', 'currency') g.set_type('credits', 'boolean') - g.set_label('upc', "UPC") - g.set_label('brand_name', "Brand") g.set_label('cases_ordered', "Cases Ord.") g.set_label('units_ordered', "Units Ord.") g.set_label('cases_received', "Cases Rec.") @@ -475,30 +582,125 @@ class PurchasingBatchView(BatchMasterView): if row.status_code in (row.STATUS_INCOMPLETE, row.STATUS_ORDERED_RECEIVED_DIFFER): return 'notice' - def _preconfigure_row_fieldset(self, fs): - super(PurchasingBatchView, self)._preconfigure_row_fieldset(fs) - fs.upc.set(label="UPC") - fs.item_id.set(label="Item ID") - fs.brand_name.set(label="Brand") - fs.case_quantity.set(renderer=forms.renderers.QuantityFieldRenderer, readonly=True) - fs.cases_ordered.set(renderer=forms.renderers.QuantityFieldRenderer) - fs.units_ordered.set(renderer=forms.renderers.QuantityFieldRenderer) - fs.cases_received.set(renderer=forms.renderers.QuantityFieldRenderer) - fs.units_received.set(renderer=forms.renderers.QuantityFieldRenderer) - fs.cases_damaged.set(renderer=forms.renderers.QuantityFieldRenderer) - fs.units_damaged.set(renderer=forms.renderers.QuantityFieldRenderer) - fs.cases_expired.set(renderer=forms.renderers.QuantityFieldRenderer) - fs.units_expired.set(renderer=forms.renderers.QuantityFieldRenderer) - fs.cases_mispick.set(renderer=forms.renderers.QuantityFieldRenderer) - fs.units_mispick.set(renderer=forms.renderers.QuantityFieldRenderer) - fs.po_line_number.set(label="PO Line Number") - 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.credits.set(readonly=True) - # fs.append(fa.Field('item_lookup', label="Item Lookup Code", required=True, - # validate=self.item_lookup)) + def configure_row_form(self, f): + super(PurchasingBatchView, self).configure_row_form(f) + row = f.model_instance + if self.creating: + batch = self.get_instance() + else: + batch = self.get_parent(row) + + # readonly fields + f.set_readonly('case_quantity') + f.set_readonly('credits') + + # quantity fields + f.set_type('case_quantity', 'quantity') + f.set_type('cases_ordered', 'quantity') + f.set_type('units_ordered', 'quantity') + f.set_type('cases_received', 'quantity') + f.set_type('units_received', 'quantity') + f.set_type('cases_damaged', 'quantity') + f.set_type('units_damaged', 'quantity') + f.set_type('cases_expired', 'quantity') + f.set_type('units_expired', 'quantity') + f.set_type('cases_mispick', 'quantity') + f.set_type('units_mispick', 'quantity') + + # currency fields + f.set_type('po_unit_cost', 'currency') + f.set_type('po_total', 'currency') + f.set_type('invoice_unit_cost', 'currency') + f.set_type('invoice_total', 'currency') + + if self.creating: + f.remove_fields( + 'upc', + 'product', + 'po_total', + 'invoice_total', + ) + if self.batch_mode == self.enum.PURCHASE_BATCH_MODE_ORDERING: + f.remove_fields('cases_received', + 'units_received') + elif self.batch_mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING: + f.remove_fields('cases_ordered', + 'units_ordered') + + elif self.editing: + f.set_readonly('upc') + f.set_readonly('product') + f.remove_fields('po_total', + 'invoice_total', + 'status_code') + + elif self.viewing: + if row.product: + f.remove_fields('brand_name', + 'description', + 'size') + else: + f.remove_field('product') + + def configure_mobile_row_form(self, f): + super(PurchasingBatchView, self).configure_mobile_row_form(f) + # row = f.model_instance + # if self.creating: + # batch = self.get_instance() + # else: + # batch = self.get_parent(row) + + # # readonly fields + # f.set_readonly('case_quantity') + # f.set_readonly('credits') + + # quantity fields + f.set_type('case_quantity', 'quantity') + f.set_type('cases_ordered', 'quantity') + f.set_type('units_ordered', 'quantity') + f.set_type('cases_received', 'quantity') + f.set_type('units_received', 'quantity') + f.set_type('cases_damaged', 'quantity') + f.set_type('units_damaged', 'quantity') + f.set_type('cases_expired', 'quantity') + f.set_type('units_expired', 'quantity') + f.set_type('cases_mispick', 'quantity') + f.set_type('units_mispick', 'quantity') + + # currency fields + f.set_type('po_unit_cost', 'currency') + f.set_type('po_total', 'currency') + f.set_type('invoice_unit_cost', 'currency') + f.set_type('invoice_total', 'currency') + + # if self.creating: + # f.remove_fields( + # 'upc', + # 'product', + # 'po_total', + # 'invoice_total', + # ) + # if self.batch_mode == self.enum.PURCHASE_BATCH_MODE_ORDERING: + # f.remove_fields('cases_received', + # 'units_received') + # elif self.batch_mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING: + # f.remove_fields('cases_ordered', + # 'units_ordered') + + # elif self.editing: + # f.set_readonly('upc') + # f.set_readonly('product') + # f.remove_fields('po_total', + # 'invoice_total', + # 'status_code') + + # elif self.viewing: + # if row.product: + # f.remove_fields('brand_name', + # 'description', + # 'size') + # else: + # f.remove_field('product') # def item_lookup(self, value, field=None): # """ @@ -519,71 +721,6 @@ class PurchasingBatchView(BatchMasterView): # return product.uuid # 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.item_id, - fs.product, - fs.brand_name, - fs.description, - fs.size, - fs.case_quantity, - fs.cases_ordered, - fs.units_ordered, - fs.cases_received, - fs.units_received, - fs.cases_damaged, - fs.units_damaged, - fs.cases_expired, - fs.units_expired, - fs.cases_mispick, - fs.units_mispick, - fs.po_line_number, - fs.po_unit_cost, - fs.po_total, - fs.invoice_line_number, - fs.invoice_unit_cost, - fs.invoice_total, - fs.status_code, - fs.credits, - ]) - - if self.creating: - del fs.upc - del fs.product - del fs.po_total - del fs.invoice_total - if self.batch_mode == self.enum.PURCHASE_BATCH_MODE_ORDERING: - del fs.cases_received - del fs.units_received - elif self.batch_mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING: - del fs.cases_ordered - del fs.units_ordered - - elif self.editing: - # del fs.item_lookup - fs.upc.set(readonly=True) - fs.product.set(readonly=True) - del fs.po_total - del fs.invoice_total - del fs.status_code - - elif self.viewing: - # del fs.item_lookup - if fs.model.product: - del (fs.brand_name, - fs.description, - fs.size) - else: - del fs.product - # def before_create_row(self, form): # row = form.fieldset.model # batch = self.get_instance() @@ -594,34 +731,38 @@ class PurchasingBatchView(BatchMasterView): # def after_create_row(self, row): # self.handler.refresh_row(row) -# def after_edit_row(self, row): -# batch = row.batch + def save_edit_row_form(self, form): + row = form.model_instance + batch = row.batch -# # first undo any totals previously in effect for the row -# if batch.mode == self.enum.PURCHASE_BATCH_MODE_ORDERING 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 + # first undo any totals previously in effect for the row + if batch.mode == self.enum.PURCHASE_BATCH_MODE_ORDERING 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) + row = super(PurchasingBatchView, self).save_edit_row_form(form) + # TODO: is this needed? + # self.handler.refresh_row(row) + return 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()) -# def delete_row(self): -# """ -# Update the PO total in addition to marking row as removed. -# """ -# row = self.Session.query(self.model_row_class).get(self.request.matchdict['uuid']) -# if not row: -# raise httpexceptions.HTTPNotFound() -# if row.po_total: -# row.batch.po_total -= row.po_total -# if row.invoice_total: -# row.batch.invoice_total -= row.invoice_total -# row.removed = True -# return self.redirect(self.get_action_url('view', row.batch)) + def delete_row(self): + """ + Update the batch totals in addition to marking row as removed. + """ + row = self.Session.query(self.model_row_class).get(self.request.matchdict['row_uuid']) + if not row: + raise self.notfound() + batch = row.batch + if row.po_total: + batch.po_total -= row.po_total + if row.invoice_total: + batch.invoice_total -= row.invoice_total + return super(PurchasingBatchView, self).delete_row() # def get_execute_success_url(self, batch, result, **kwargs): # # if batch execution yielded a Purchase, redirect to it diff --git a/tailbone/views/purchasing/ordering.py b/tailbone/views/purchasing/ordering.py index f319f9e9..5b4e7421 100644 --- a/tailbone/views/purchasing/ordering.py +++ b/tailbone/views/purchasing/ordering.py @@ -38,7 +38,6 @@ from rattail.time import localtime from pyramid.response import FileResponse -from tailbone import forms from tailbone.views.purchasing import PurchasingBatchView @@ -55,6 +54,21 @@ class OrderingBatchView(PurchasingBatchView): rows_editable = True mobile_rows_editable = True + mobile_form_fields = [ + 'vendor', + 'department', + 'date_ordered', + 'po_number', + 'po_total', + 'created', + 'created_by', + 'notes', + 'status_code', + 'complete', + 'executed', + 'executed_by', + ] + row_grid_columns = [ 'sequence', 'upc', @@ -233,7 +247,9 @@ class OrderingBatchView(PurchasingBatchView): if cases_ordered or units_ordered: row.cases_ordered = cases_ordered or None row.units_ordered = units_ordered or None - row.removed = False + if row.removed: + row.removed = False + batch.rowcount += 1 self.handler.refresh_row(row) else: row.removed = True @@ -246,6 +262,7 @@ class OrderingBatchView(PurchasingBatchView): row.cases_ordered = cases_ordered or None row.units_ordered = units_ordered or None self.handler.refresh_row(row) + batch.rowcount += 1 return { 'row_cases_ordered': '' if not row or row.removed else int(row.cases_ordered or 0), @@ -292,31 +309,6 @@ class OrderingBatchView(PurchasingBatchView): data['mode_title'] = self.enum.PURCHASE_BATCH_MODE[mode].capitalize() return self.render_to_response('create', data, mobile=True) - def preconfigure_mobile_fieldset(self, fs): - super(OrderingBatchView, self).preconfigure_mobile_fieldset(fs) - fs.vendor.set(attrs={'hyperlink': False}) - - def configure_mobile_fieldset(self, fs): - fields = [ - fs.vendor, - fs.department, - fs.date_ordered, - fs.po_number, - fs.po_total, - fs.created, - fs.created_by, - fs.notes, - fs.status_code, - fs.complete, - ] - batch = fs.model - if (self.viewing or self.deleting) and batch.executed: - fields.extend([ - fs.executed, - fs.executed_by, - ]) - fs.configure(include=fields) - def download_excel(self): """ Download ordering batch as Excel spreadsheet. diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index 87487cd6..2e6c53e1 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2017 Lance Edgar +# Copyright © 2010-2018 Lance Edgar # # This file is part of Rattail. # @@ -35,7 +35,6 @@ from rattail.db import model, api from rattail.gpc import GPC from rattail.util import pretty_quantity, prettify -import formalchemy as fa import formencode as fe from webhelpers2.html import tags @@ -95,6 +94,11 @@ class ReceivingBatchView(PurchasingBatchView): mobile_rows_filterable = True mobile_rows_creatable = True + mobile_form_fields = [ + 'vendor', + 'department', + ] + row_grid_columns = [ 'sequence', 'upc', @@ -112,6 +116,14 @@ class ReceivingBatchView(PurchasingBatchView): 'status_code', ] + row_form_fields = [ + 'vendor', + 'department', + 'complete', + 'executed', + 'executed_by', + ] + @property def batch_mode(self): return self.enum.PURCHASE_BATCH_MODE_RECEIVING @@ -186,21 +198,14 @@ class ReceivingBatchView(PurchasingBatchView): kwargs['sms_transaction_number'] = batch.sms_transaction_number return kwargs - def configure_mobile_fieldset(self, fs): - fs.configure(include=[ - fs.vendor.with_renderer(fa.TextFieldRenderer), - fs.department.with_renderer(fa.TextFieldRenderer), - fs.complete, - fs.executed, - fs.executed_by, - ]) - batch = fs.model - if not batch.executed: - del [fs.executed, fs.executed_by] - if not batch.complete: - del fs.complete - else: - del fs.complete + def configure_mobile_form(self, f): + super(ReceivingBatchView, self).configure_mobile_form(f) + + # vendor + # fs.vendor.with_renderer(fa.TextFieldRenderer), + + # department + # fs.department.with_renderer(fa.TextFieldRenderer), def render_mobile_row_listitem(self, row, i): description = row.product.full_description if row.product else row.description