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
"""
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())

View file

@ -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)

View file

@ -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)

View file

@ -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)