Allow "arbitrary" PO attachment to purchase batch
for sake of other POS integration etc.
This commit is contained in:
parent
08a75f6e9f
commit
1be26b7f33
|
@ -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,12 +407,30 @@ 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)
|
||||||
url = self.request.route_url('purchases.view', uuid=purchase.uuid)
|
if isinstance(purchase, model.Purchase):
|
||||||
return tags.link_to(text, url)
|
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):
|
def render_vendor_email(self, batch, field):
|
||||||
if batch.vendor.email:
|
if batch.vendor.email:
|
||||||
|
@ -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,16 +560,20 @@ 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):
|
||||||
purchase = batch.purchase
|
field = self.batch_handler.get_purchase_order_fieldname()
|
||||||
if not purchase and batch.purchase_uuid:
|
if field == 'purchase':
|
||||||
purchase = self.Session.get(model.Purchase, batch.purchase_uuid)
|
purchase = batch.purchase
|
||||||
assert purchase
|
if not purchase and batch.purchase_uuid:
|
||||||
if purchase:
|
purchase = self.Session.get(model.Purchase, batch.purchase_uuid)
|
||||||
kwargs['purchase'] = purchase
|
assert purchase
|
||||||
kwargs['buyer'] = purchase.buyer
|
if purchase:
|
||||||
kwargs['buyer_uuid'] = purchase.buyer_uuid
|
kwargs['purchase'] = purchase
|
||||||
kwargs['date_ordered'] = purchase.date_ordered
|
kwargs['buyer'] = purchase.buyer
|
||||||
kwargs['po_total'] = purchase.po_total
|
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
|
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())
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue