Allow "arbitrary" PO attachment to purchase batch

for sake of other POS integration etc.
This commit is contained in:
Lance Edgar 2023-06-27 12:37:16 -05:00
parent 08a75f6e9f
commit 1be26b7f33
4 changed files with 74 additions and 111 deletions

View file

@ -24,7 +24,7 @@
Base class for purchasing batch views Base class for purchasing batch views
""" """
from rattail.db import model, api from rattail.db.model import PurchaseBatch, PurchaseBatchRow
import colander import colander
from deform import widget as dfwidget from deform import widget as dfwidget
@ -40,8 +40,8 @@ class PurchasingBatchView(BatchMasterView):
Master view base class, for purchase batches. The views for both Master view base class, for purchase batches. The views for both
"ordering" and "receiving" batches will inherit from this. "ordering" and "receiving" batches will inherit from this.
""" """
model_class = model.PurchaseBatch model_class = PurchaseBatch
model_row_class = model.PurchaseBatchRow model_row_class = PurchaseBatchRow
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler' default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
supports_new_product = False supports_new_product = False
cloneable = True cloneable = True
@ -160,11 +160,13 @@ class PurchasingBatchView(BatchMasterView):
raise NotImplementedError("Please define `batch_mode` for your purchasing batch view") raise NotImplementedError("Please define `batch_mode` for your purchasing batch view")
def query(self, session): def query(self, session):
model = self.model
return session.query(model.PurchaseBatch)\ return session.query(model.PurchaseBatch)\
.filter(model.PurchaseBatch.mode == self.batch_mode) .filter(model.PurchaseBatch.mode == self.batch_mode)
def configure_grid(self, g): 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.joiners['vendor'] = lambda q: q.join(model.Vendor)
g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name, g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name,
@ -309,7 +311,7 @@ class PurchasingBatchView(BatchMasterView):
if buyer: if buyer:
buyer_display = str(buyer) buyer_display = str(buyer)
elif self.creating: elif self.creating:
buyer = self.request.user.employee buyer = app.get_employee(self.request.user)
if buyer: if buyer:
buyer_display = str(buyer) buyer_display = str(buyer)
f.set_default('buyer_uuid', buyer.uuid) f.set_default('buyer_uuid', buyer.uuid)
@ -405,13 +407,31 @@ class PurchasingBatchView(BatchMasterView):
return tags.link_to(text, url) return tags.link_to(text, url)
def render_purchase(self, batch, field): 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: if not purchase:
return "" return
# render link to native purchase, if possible
text = str(purchase) text = str(purchase)
if isinstance(purchase, model.Purchase):
url = self.request.route_url('purchases.view', uuid=purchase.uuid) url = self.request.route_url('purchases.view', uuid=purchase.uuid)
return tags.link_to(text, url) return tags.link_to(text, url)
# otherwise just render purchase as-is
return text
def render_vendor_email(self, batch, field): def render_vendor_email(self, batch, field):
if batch.vendor.email: if batch.vendor.email:
return batch.vendor.email.address return batch.vendor.email.address
@ -448,12 +468,14 @@ class PurchasingBatchView(BatchMasterView):
return text return text
def get_store_values(self): def get_store_values(self):
model = self.model
stores = self.Session.query(model.Store)\ stores = self.Session.query(model.Store)\
.order_by(model.Store.id) .order_by(model.Store.id)
return [(s.uuid, "({}) {}".format(s.id, s.name)) return [(s.uuid, "({}) {}".format(s.id, s.name))
for s in stores] for s in stores]
def get_vendors(self): def get_vendors(self):
model = self.model
return self.Session.query(model.Vendor)\ return self.Session.query(model.Vendor)\
.order_by(model.Vendor.name) .order_by(model.Vendor.name)
@ -463,6 +485,7 @@ class PurchasingBatchView(BatchMasterView):
for v in vendors] for v in vendors]
def get_buyers(self): def get_buyers(self):
model = self.model
return self.Session.query(model.Employee)\ return self.Session.query(model.Employee)\
.join(model.Person)\ .join(model.Person)\
.filter(model.Employee.status == self.enum.EMPLOYEE_STATUS_CURRENT)\ .filter(model.Employee.status == self.enum.EMPLOYEE_STATUS_CURRENT)\
@ -474,6 +497,7 @@ class PurchasingBatchView(BatchMasterView):
for b in buyers] for b in buyers]
def get_department_options(self): def get_department_options(self):
model = self.model
departments = self.Session.query(model.Department).order_by(model.Department.number) departments = self.Session.query(model.Department).order_by(model.Department.number)
return [('{} {}'.format(d.number, d.name), d.uuid) for d in departments] return [('{} {}'.format(d.number, d.name), d.uuid) for d in departments]
@ -487,39 +511,10 @@ class PurchasingBatchView(BatchMasterView):
if phone.type == 'Fax': if phone.type == 'Fax':
return phone.number 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): 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['mode'] = self.batch_mode
kwargs['truck_dump'] = batch.truck_dump kwargs['truck_dump'] = batch.truck_dump
kwargs['invoice_parser_key'] = batch.invoice_parser_key kwargs['invoice_parser_key'] = batch.invoice_parser_key
@ -565,6 +560,8 @@ class PurchasingBatchView(BatchMasterView):
if self.batch_mode in (self.enum.PURCHASE_BATCH_MODE_RECEIVING, if self.batch_mode in (self.enum.PURCHASE_BATCH_MODE_RECEIVING,
self.enum.PURCHASE_BATCH_MODE_COSTING): self.enum.PURCHASE_BATCH_MODE_COSTING):
field = self.batch_handler.get_purchase_order_fieldname()
if field == 'purchase':
purchase = batch.purchase purchase = batch.purchase
if not purchase and batch.purchase_uuid: if not purchase and batch.purchase_uuid:
purchase = self.Session.get(model.Purchase, batch.purchase_uuid) purchase = self.Session.get(model.Purchase, batch.purchase_uuid)
@ -575,6 +572,8 @@ class PurchasingBatchView(BatchMasterView):
kwargs['buyer_uuid'] = purchase.buyer_uuid kwargs['buyer_uuid'] = purchase.buyer_uuid
kwargs['date_ordered'] = purchase.date_ordered kwargs['date_ordered'] = purchase.date_ordered
kwargs['po_total'] = purchase.po_total kwargs['po_total'] = purchase.po_total
elif hasattr(batch, field):
kwargs[field] = getattr(batch, field)
return kwargs return kwargs
@ -826,25 +825,6 @@ class PurchasingBatchView(BatchMasterView):
return HTML.literal( return HTML.literal(
g.render_buefy_table_element(data_prop='rowData.credits')) 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): # def before_create_row(self, form):
# row = form.fieldset.model # row = form.fieldset.model
# batch = self.get_instance() # batch = self.get_instance()
@ -937,28 +917,6 @@ class PurchasingBatchView(BatchMasterView):
# return self.get_action_url('view', batch) # 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): class NewProduct(colander.Schema):
item_id = colander.SchemaNode(colander.String()) item_id = colander.SchemaNode(colander.String())

View file

@ -43,8 +43,6 @@ class CostingBatchView(PurchasingBatchView):
downloadable = True downloadable = True
bulk_deletable = True bulk_deletable = True
purchase_order_fieldname = 'purchase'
labels = { labels = {
'invoice_parser_key': "Invoice Parser", 'invoice_parser_key': "Invoice Parser",
} }
@ -290,8 +288,9 @@ class CostingBatchView(PurchasingBatchView):
f.remove_field('batch_type') f.remove_field('batch_type')
# purchase # purchase
field = self.batch_handler.get_purchase_order_fieldname()
if (self.creating and workflow == 'invoice_with_po' if (self.creating and workflow == 'invoice_with_po'
and self.purchase_order_fieldname == 'purchase'): and field == 'purchase'):
f.replace('purchase', 'purchase_uuid') f.replace('purchase', 'purchase_uuid')
purchases = self.handler.get_eligible_purchases( purchases = self.handler.get_eligible_purchases(
vendor, self.enum.PURCHASE_BATCH_MODE_COSTING) vendor, self.enum.PURCHASE_BATCH_MODE_COSTING)
@ -317,7 +316,6 @@ class CostingBatchView(PurchasingBatchView):
@classmethod @classmethod
def defaults(cls, config): def defaults(cls, config):
cls._costing_defaults(config) cls._costing_defaults(config)
cls._purchasing_defaults(config)
cls._batch_defaults(config) cls._batch_defaults(config)
cls._defaults(config) cls._defaults(config)

View file

@ -486,7 +486,6 @@ class OrderingBatchView(PurchasingBatchView):
@classmethod @classmethod
def defaults(cls, config): def defaults(cls, config):
cls._ordering_defaults(config) cls._ordering_defaults(config)
cls._purchasing_defaults(config)
cls._batch_defaults(config) cls._batch_defaults(config)
cls._defaults(config) cls._defaults(config)

View file

@ -87,8 +87,6 @@ class ReceivingBatchView(PurchasingBatchView):
default_uom_is_case = True default_uom_is_case = True
purchase_order_fieldname = 'purchase'
labels = { labels = {
'truck_dump_batch': "Truck Dump Parent", 'truck_dump_batch': "Truck Dump Parent",
'invoice_parser_key': "Invoice Parser", 'invoice_parser_key': "Invoice Parser",
@ -390,7 +388,7 @@ class ReceivingBatchView(PurchasingBatchView):
return title return title
def configure_form(self, f): def configure_form(self, f):
super(ReceivingBatchView, self).configure_form(f) super().configure_form(f)
model = self.model model = self.model
batch = f.model_instance batch = f.model_instance
allow_truck_dump = self.batch_handler.allow_truck_dump_receiving() allow_truck_dump = self.batch_handler.allow_truck_dump_receiving()
@ -498,18 +496,28 @@ class ReceivingBatchView(PurchasingBatchView):
f.set_widget('store_uuid', dfwidget.HiddenWidget()) f.set_widget('store_uuid', dfwidget.HiddenWidget())
# purchase # purchase
if (self.creating and workflow in ('from_po', 'from_po_with_invoice') field = self.batch_handler.get_purchase_order_fieldname()
and self.purchase_order_fieldname == 'purchase'): if field == 'purchase':
f.replace('purchase', 'purchase_uuid') 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( purchases = self.batch_handler.get_eligible_purchases(
vendor, self.enum.PURCHASE_BATCH_MODE_RECEIVING) vendor, self.batch_mode)
values = [(p.uuid, self.batch_handler.render_eligible_purchase(p)) values = [(self.batch_handler.get_eligible_purchase_key(p),
self.batch_handler.render_eligible_purchase(p))
for p in purchases] for p in purchases]
f.set_widget('purchase_uuid', dfwidget.SelectWidget(values=values)) f.set_widget(field, dfwidget.SelectWidget(values=values))
f.set_label('purchase_uuid', "Purchase Order") if field == 'purchase_uuid':
f.set_required('purchase_uuid') f.set_label(field, "Purchase Order")
elif self.creating or not batch.purchase: f.set_required(field)
elif self.creating:
f.remove_field('purchase') f.remove_field('purchase')
else: # not creating
if field != 'purchase_uuid':
f.replace('purchase', field)
f.set_renderer(field, self.render_purchase)
# department # department
if self.creating: if self.creating:
@ -939,8 +947,9 @@ class ReceivingBatchView(PurchasingBatchView):
Assign the original purchase order to the given batch. Default Assign the original purchase order to the given batch. Default
behavior assumes a Rattail Purchase object is what we're after. 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( purchase = self.handler.assign_purchase_order(
batch, po_form.validated[self.purchase_order_fieldname], batch, po_form.validated[field],
session=self.Session()) session=self.Session())
department = self.department_for_purchase(purchase) department = self.department_for_purchase(purchase)
@ -1992,7 +2001,6 @@ class ReceivingBatchView(PurchasingBatchView):
@classmethod @classmethod
def defaults(cls, config): def defaults(cls, config):
cls._receiving_defaults(config) cls._receiving_defaults(config)
cls._purchasing_defaults(config)
cls._batch_defaults(config) cls._batch_defaults(config)
cls._defaults(config) cls._defaults(config)