From 1be26b7f33bc11d1446771044bd3a1f07005bbdf Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 27 Jun 2023 12:37:16 -0500 Subject: [PATCH] Allow "arbitrary" PO attachment to purchase batch for sake of other POS integration etc. --- tailbone/views/purchasing/batch.py | 142 +++++++++---------------- tailbone/views/purchasing/costing.py | 6 +- tailbone/views/purchasing/ordering.py | 1 - tailbone/views/purchasing/receiving.py | 36 ++++--- 4 files changed, 74 insertions(+), 111 deletions(-) diff --git a/tailbone/views/purchasing/batch.py b/tailbone/views/purchasing/batch.py index 16153f64..8960a522 100644 --- a/tailbone/views/purchasing/batch.py +++ b/tailbone/views/purchasing/batch.py @@ -24,7 +24,7 @@ Base class for purchasing batch views """ -from rattail.db import model, api +from rattail.db.model import PurchaseBatch, PurchaseBatchRow import colander from deform import widget as dfwidget @@ -40,8 +40,8 @@ class PurchasingBatchView(BatchMasterView): Master view base class, for purchase batches. The views for both "ordering" and "receiving" batches will inherit from this. """ - model_class = model.PurchaseBatch - model_row_class = model.PurchaseBatchRow + model_class = PurchaseBatch + model_row_class = PurchaseBatchRow default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler' supports_new_product = False cloneable = True @@ -160,11 +160,13 @@ class PurchasingBatchView(BatchMasterView): raise NotImplementedError("Please define `batch_mode` for your purchasing batch view") def query(self, session): + model = self.model return session.query(model.PurchaseBatch)\ .filter(model.PurchaseBatch.mode == self.batch_mode) def configure_grid(self, g): - super(PurchasingBatchView, self).configure_grid(g) + super().configure_grid(g) + model = self.model g.joiners['vendor'] = lambda q: q.join(model.Vendor) g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name, @@ -309,7 +311,7 @@ class PurchasingBatchView(BatchMasterView): if buyer: buyer_display = str(buyer) elif self.creating: - buyer = self.request.user.employee + buyer = app.get_employee(self.request.user) if buyer: buyer_display = str(buyer) f.set_default('buyer_uuid', buyer.uuid) @@ -405,12 +407,30 @@ class PurchasingBatchView(BatchMasterView): return tags.link_to(text, url) def render_purchase(self, batch, field): - purchase = batch.purchase + model = self.model + + # default logic can only render the "normal" (built-in) + # purchase field; anything else must be handled by view + # supplement if possible + if field != 'purchase': + for supp in self.iter_view_supplements(): + renderer = getattr(supp, f'render_purchase_{field}', None) + if renderer: + return renderer(batch) + + # nothing to render if no purchase found + purchase = getattr(batch, field) if not purchase: - return "" + return + + # render link to native purchase, if possible text = str(purchase) - url = self.request.route_url('purchases.view', uuid=purchase.uuid) - return tags.link_to(text, url) + if isinstance(purchase, model.Purchase): + url = self.request.route_url('purchases.view', uuid=purchase.uuid) + return tags.link_to(text, url) + + # otherwise just render purchase as-is + return text def render_vendor_email(self, batch, field): if batch.vendor.email: @@ -448,12 +468,14 @@ class PurchasingBatchView(BatchMasterView): return text def get_store_values(self): + model = self.model 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): + model = self.model return self.Session.query(model.Vendor)\ .order_by(model.Vendor.name) @@ -463,6 +485,7 @@ class PurchasingBatchView(BatchMasterView): for v in vendors] def get_buyers(self): + model = self.model return self.Session.query(model.Employee)\ .join(model.Person)\ .filter(model.Employee.status == self.enum.EMPLOYEE_STATUS_CURRENT)\ @@ -474,6 +497,7 @@ class PurchasingBatchView(BatchMasterView): for b in buyers] def get_department_options(self): + model = self.model departments = self.Session.query(model.Department).order_by(model.Department.number) return [('{} {}'.format(d.number, d.name), d.uuid) for d in departments] @@ -487,39 +511,10 @@ class PurchasingBatchView(BatchMasterView): if phone.type == 'Fax': return phone.number - def eligible_purchases(self, vendor_uuid=None, mode=None): - if not vendor_uuid: - vendor_uuid = self.request.GET.get('vendor_uuid') - vendor = self.Session.get(model.Vendor, vendor_uuid) if vendor_uuid else None - if not vendor: - return {'error': "Must specify a vendor."} - - if mode is None: - 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 = self.handler.get_eligible_purchases(vendor, mode) - return self.get_eligible_purchases_data(purchases) - - def get_eligible_purchases_data(self, purchases): - return {'purchases': [{'key': p.uuid, - 'department_uuid': p.department_uuid or '', - 'display': self.render_eligible_purchase(p)} - for p in purchases]} - - def render_eligible_purchase(self, purchase): - if purchase.status == self.enum.PURCHASE_STATUS_ORDERED: - date = purchase.date_ordered - total = purchase.po_total - elif purchase.status == self.enum.PURCHASE_STATUS_RECEIVED: - date = purchase.date_received - total = purchase.invoice_total - return '{} for ${:0,.2f} ({})'.format(date, total or 0, purchase.department or purchase.buyer) - def get_batch_kwargs(self, batch, **kwargs): - kwargs = super(PurchasingBatchView, self).get_batch_kwargs(batch, **kwargs) + kwargs = super().get_batch_kwargs(batch, **kwargs) + model = self.model + kwargs['mode'] = self.batch_mode kwargs['truck_dump'] = batch.truck_dump kwargs['invoice_parser_key'] = batch.invoice_parser_key @@ -565,16 +560,20 @@ class PurchasingBatchView(BatchMasterView): if self.batch_mode in (self.enum.PURCHASE_BATCH_MODE_RECEIVING, self.enum.PURCHASE_BATCH_MODE_COSTING): - purchase = batch.purchase - if not purchase and batch.purchase_uuid: - purchase = self.Session.get(model.Purchase, batch.purchase_uuid) - assert purchase - if 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 + field = self.batch_handler.get_purchase_order_fieldname() + if field == 'purchase': + purchase = batch.purchase + if not purchase and batch.purchase_uuid: + purchase = self.Session.get(model.Purchase, batch.purchase_uuid) + assert purchase + if 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 + elif hasattr(batch, field): + kwargs[field] = getattr(batch, field) return kwargs @@ -826,25 +825,6 @@ class PurchasingBatchView(BatchMasterView): return HTML.literal( g.render_buefy_table_element(data_prop='rowData.credits')) -# def item_lookup(self, value, field=None): -# """ -# Try to locate a single product using ``value`` as a lookup code. -# """ -# batch = self.get_instance() -# product = api.get_product_by_vendor_code(Session(), value, vendor=batch.vendor) -# if product: -# return product.uuid -# if value.isdigit(): -# product = api.get_product_by_upc(Session(), GPC(value)) -# if not product: -# product = api.get_product_by_upc(Session(), GPC(value, calc_check_digit='upc')) -# if product: -# if not product.cost_for_vendor(batch.vendor): -# raise fa.ValidationError("Product {} exists but has no cost for vendor {}".format( -# product.upc.pretty(), batch.vendor)) -# return product.uuid -# raise fa.ValidationError("Product not found") - # def before_create_row(self, form): # row = form.fieldset.model # batch = self.get_instance() @@ -937,28 +917,6 @@ class PurchasingBatchView(BatchMasterView): # return self.get_action_url('view', batch) - @classmethod - def _purchasing_defaults(cls, config): - rattail_config = config.registry.settings.get('rattail_config') - route_prefix = cls.get_route_prefix() - url_prefix = cls.get_url_prefix() - permission_prefix = cls.get_permission_prefix() - 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)) - - - @classmethod - def defaults(cls, config): - cls._purchasing_defaults(config) - cls._batch_defaults(config) - cls._defaults(config) - - class NewProduct(colander.Schema): item_id = colander.SchemaNode(colander.String()) diff --git a/tailbone/views/purchasing/costing.py b/tailbone/views/purchasing/costing.py index 294b29ef..ec4e3ee3 100644 --- a/tailbone/views/purchasing/costing.py +++ b/tailbone/views/purchasing/costing.py @@ -43,8 +43,6 @@ class CostingBatchView(PurchasingBatchView): downloadable = True bulk_deletable = True - purchase_order_fieldname = 'purchase' - labels = { 'invoice_parser_key': "Invoice Parser", } @@ -290,8 +288,9 @@ class CostingBatchView(PurchasingBatchView): f.remove_field('batch_type') # purchase + field = self.batch_handler.get_purchase_order_fieldname() if (self.creating and workflow == 'invoice_with_po' - and self.purchase_order_fieldname == 'purchase'): + and field == 'purchase'): f.replace('purchase', 'purchase_uuid') purchases = self.handler.get_eligible_purchases( vendor, self.enum.PURCHASE_BATCH_MODE_COSTING) @@ -317,7 +316,6 @@ class CostingBatchView(PurchasingBatchView): @classmethod def defaults(cls, config): cls._costing_defaults(config) - cls._purchasing_defaults(config) cls._batch_defaults(config) cls._defaults(config) diff --git a/tailbone/views/purchasing/ordering.py b/tailbone/views/purchasing/ordering.py index b0b00402..03308d07 100644 --- a/tailbone/views/purchasing/ordering.py +++ b/tailbone/views/purchasing/ordering.py @@ -486,7 +486,6 @@ class OrderingBatchView(PurchasingBatchView): @classmethod def defaults(cls, config): cls._ordering_defaults(config) - cls._purchasing_defaults(config) cls._batch_defaults(config) cls._defaults(config) diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py index cdc69fe5..e659123a 100644 --- a/tailbone/views/purchasing/receiving.py +++ b/tailbone/views/purchasing/receiving.py @@ -87,8 +87,6 @@ class ReceivingBatchView(PurchasingBatchView): default_uom_is_case = True - purchase_order_fieldname = 'purchase' - labels = { 'truck_dump_batch': "Truck Dump Parent", 'invoice_parser_key': "Invoice Parser", @@ -390,7 +388,7 @@ class ReceivingBatchView(PurchasingBatchView): return title def configure_form(self, f): - super(ReceivingBatchView, self).configure_form(f) + super().configure_form(f) model = self.model batch = f.model_instance allow_truck_dump = self.batch_handler.allow_truck_dump_receiving() @@ -498,18 +496,28 @@ class ReceivingBatchView(PurchasingBatchView): f.set_widget('store_uuid', dfwidget.HiddenWidget()) # purchase - if (self.creating and workflow in ('from_po', 'from_po_with_invoice') - and self.purchase_order_fieldname == 'purchase'): - f.replace('purchase', 'purchase_uuid') + field = self.batch_handler.get_purchase_order_fieldname() + if field == 'purchase': + field = 'purchase_uuid' + # TODO: workflow "invoice_with_po" is for costing mode, should rename? + if self.creating and workflow in ( + 'from_po', 'from_po_with_invoice', 'invoice_with_po'): + f.replace('purchase', field) purchases = self.batch_handler.get_eligible_purchases( - vendor, self.enum.PURCHASE_BATCH_MODE_RECEIVING) - values = [(p.uuid, self.batch_handler.render_eligible_purchase(p)) + vendor, self.batch_mode) + values = [(self.batch_handler.get_eligible_purchase_key(p), + self.batch_handler.render_eligible_purchase(p)) for p in purchases] - f.set_widget('purchase_uuid', dfwidget.SelectWidget(values=values)) - f.set_label('purchase_uuid', "Purchase Order") - f.set_required('purchase_uuid') - elif self.creating or not batch.purchase: + f.set_widget(field, dfwidget.SelectWidget(values=values)) + if field == 'purchase_uuid': + f.set_label(field, "Purchase Order") + f.set_required(field) + elif self.creating: f.remove_field('purchase') + else: # not creating + if field != 'purchase_uuid': + f.replace('purchase', field) + f.set_renderer(field, self.render_purchase) # department if self.creating: @@ -939,8 +947,9 @@ class ReceivingBatchView(PurchasingBatchView): Assign the original purchase order to the given batch. Default behavior assumes a Rattail Purchase object is what we're after. """ + field = self.batch_handler.get_purchase_order_fieldname() purchase = self.handler.assign_purchase_order( - batch, po_form.validated[self.purchase_order_fieldname], + batch, po_form.validated[field], session=self.Session()) department = self.department_for_purchase(purchase) @@ -1992,7 +2001,6 @@ class ReceivingBatchView(PurchasingBatchView): @classmethod def defaults(cls, config): cls._receiving_defaults(config) - cls._purchasing_defaults(config) cls._batch_defaults(config) cls._defaults(config)