Add new 'receiving form' for purchase batches
This commit is contained in:
parent
468a84aa90
commit
6c3d221e98
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2015 Lance Edgar
|
# Copyright © 2010-2016 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,6 +24,8 @@
|
||||||
Forms
|
Forms
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from formencode import Schema
|
from formencode import Schema
|
||||||
|
|
||||||
from .core import Form, Field, FieldSet, GenericFieldSet
|
from .core import Form, Field, FieldSet, GenericFieldSet
|
||||||
|
|
147
tailbone/templates/purchases/batches/receive_form.mako
Normal file
147
tailbone/templates/purchases/batches/receive_form.mako
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
## -*- coding: utf-8 -*-
|
||||||
|
<%inherit file="/base.mako" />
|
||||||
|
|
||||||
|
<%def name="title()">Receiving Form (${batch.vendor})</%def>
|
||||||
|
|
||||||
|
<%def name="head_tags()">
|
||||||
|
${parent.head_tags()}
|
||||||
|
${h.javascript_link(request.static_url('tailbone:static/js/numeric.js'))}
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
function invalid_product(msg) {
|
||||||
|
$('#product-info p').text(msg);
|
||||||
|
$('#product-info img').hide();
|
||||||
|
$('#product-info .rogue-item-warning').hide();
|
||||||
|
$('#product-textbox').focus().select();
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
|
||||||
|
$('#product-textbox').keydown(function(event) {
|
||||||
|
|
||||||
|
if (key_allowed(event)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (key_modifies(event)) {
|
||||||
|
$('#product').val('');
|
||||||
|
$('#product-info p').html(' ');
|
||||||
|
$('#product-info img').hide();
|
||||||
|
$('#product-info .rogue-item-warning').hide();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (event.which == 13) {
|
||||||
|
var input = $(this);
|
||||||
|
var data = {upc: input.val()};
|
||||||
|
$.get('${url('purchases.batch.receiving_lookup', uuid=batch.uuid)}', data, function(data) {
|
||||||
|
if (data.error) {
|
||||||
|
alert(data.error);
|
||||||
|
if (data.redirect) {
|
||||||
|
$('#receiving-form').mask("Redirecting...");
|
||||||
|
location.href = data.redirect;
|
||||||
|
}
|
||||||
|
} else if (data.product) {
|
||||||
|
input.val(data.product.upc_pretty);
|
||||||
|
if (data.product.cost_found) {
|
||||||
|
$('#product').val(data.product.uuid);
|
||||||
|
$('#product-info p').text(data.product.full_description);
|
||||||
|
$('#product-info img').attr('src', data.product.image_url).show();
|
||||||
|
$('#product-info .rogue-item-warning').hide();
|
||||||
|
$('#cases').focus().select();
|
||||||
|
if (! data.product.found_in_batch) {
|
||||||
|
$('#product-info .rogue-item-warning').show();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
invalid_product('cost not found for ' + data.product.full_description);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
invalid_product('product not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#product-textbox').focus();
|
||||||
|
|
||||||
|
$('#received').click(function() {
|
||||||
|
$(this).button('disable').button('option', 'label', "Working...");
|
||||||
|
$('#mode').val('received');
|
||||||
|
$('#receiving-form').submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#receiving-form').submit(function() {
|
||||||
|
$(this).mask("Working...");
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style type="text/css">
|
||||||
|
|
||||||
|
#product-info {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#product-info p {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#product-info .img-wrapper {
|
||||||
|
height: 150px;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#product-info .rogue-item-warning {
|
||||||
|
background: #f66;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
|
<%def name="context_menu_items()">
|
||||||
|
<li>${h.link_to("Back to Purchase Batch", url('purchases.batch.view', uuid=batch.uuid))}</li>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
|
<ul id="context-menu">
|
||||||
|
${self.context_menu_items()}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="form-wrapper">
|
||||||
|
${form.begin(id='receiving-form')}
|
||||||
|
${h.hidden('mode')}
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="product-textbox">Product</label>
|
||||||
|
<div class="field">
|
||||||
|
${h.hidden('product')}
|
||||||
|
<div>${h.text('product-textbox', autocomplete='off')}</div>
|
||||||
|
<div id="product-info">
|
||||||
|
<p> </p>
|
||||||
|
<div class="img-wrapper"><img /></div>
|
||||||
|
<div class="rogue-item-warning">warning: product not found on current purchase</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="cases">Cases</label>
|
||||||
|
<div class="field">${h.text('cases')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label for="units">Units</label>
|
||||||
|
<div class="field">${h.text('units')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<button type="button" id="received">Received</button>
|
||||||
|
<button type="button" id="damaged" disabled="disabled">Damaged</button>
|
||||||
|
<button type="button" id="expired" disabled="disabled">Expired</button>
|
||||||
|
<button type="button" id="mispick" disabled="disabled">Mispick</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${form.end()}
|
||||||
|
</div>
|
|
@ -18,13 +18,20 @@
|
||||||
location.href = '${url('purchases.batch.order_form', uuid=batch.uuid)}';
|
location.href = '${url('purchases.batch.order_form', uuid=batch.uuid)}';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#receive-form').click(function() {
|
||||||
|
$(this).button('disable').button('option', 'label', "Working, please wait...");
|
||||||
|
location.href = '${url('purchases.batch.receiving_form', uuid=batch.uuid)}';
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="leading_buttons()">
|
<%def name="leading_buttons()">
|
||||||
% if batch.mode == enum.PURCHASE_BATCH_MODE_NEW and not batch.complete and not batch.executed and request.has_perm('purchases.batch.order_form'):
|
% if batch.mode == enum.PURCHASE_BATCH_MODE_NEW and not batch.complete and not batch.executed and request.has_perm('purchases.batch.order_form'):
|
||||||
<button type="button" id="order-form">View as Order Form</button>
|
<button type="button" id="order-form">Ordering Form</button>
|
||||||
|
% elif batch.mode == enum.PURCHASE_BATCH_MODE_RECEIVING and not batch.complete and not batch.executed and request.has_perm('purchases.batch.receiving_form'):
|
||||||
|
<button type="button" id="receive-form">Receiving Form</button>
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
|
|
@ -335,6 +335,46 @@ class ProductsView(MasterView):
|
||||||
'instance_title': self.get_instance_title(instance),
|
'instance_title': self.get_instance_title(instance),
|
||||||
'form': form})
|
'form': form})
|
||||||
|
|
||||||
|
def search(self):
|
||||||
|
"""
|
||||||
|
Locate a product(s) by UPC.
|
||||||
|
|
||||||
|
Eventually this should be more generic, or at least offer more fields for
|
||||||
|
search. For now it operates only on the ``Product.upc`` field.
|
||||||
|
"""
|
||||||
|
data = None
|
||||||
|
upc = self.request.GET.get('upc', '').strip()
|
||||||
|
upc = re.sub(r'\D', '', upc)
|
||||||
|
if upc:
|
||||||
|
product = api.get_product_by_upc(Session(), upc)
|
||||||
|
if not product:
|
||||||
|
# Try again, assuming caller did not include check digit.
|
||||||
|
upc = GPC(upc, calc_check_digit='upc')
|
||||||
|
product = api.get_product_by_upc(Session(), upc)
|
||||||
|
if product and (not product.deleted or self.request.has_perm('products.view_deleted')):
|
||||||
|
data = {
|
||||||
|
'uuid': product.uuid,
|
||||||
|
'upc': unicode(product.upc),
|
||||||
|
'upc_pretty': product.upc.pretty(),
|
||||||
|
'full_description': product.full_description,
|
||||||
|
'image_url': pod.get_image_url(self.rattail_config, product.upc),
|
||||||
|
}
|
||||||
|
uuid = self.request.GET.get('with_vendor_cost')
|
||||||
|
if uuid:
|
||||||
|
vendor = Session.query(model.Vendor).get(uuid)
|
||||||
|
if not vendor:
|
||||||
|
return {'error': "Vendor not found"}
|
||||||
|
cost = product.cost_for_vendor(vendor)
|
||||||
|
if cost:
|
||||||
|
data['cost_found'] = True
|
||||||
|
if int(cost.case_size) == cost.case_size:
|
||||||
|
data['cost_case_size'] = int(cost.case_size)
|
||||||
|
else:
|
||||||
|
data['cost_case_size'] = '{:0.4f}'.format(cost.case_size)
|
||||||
|
else:
|
||||||
|
data['cost_found'] = False
|
||||||
|
return {'product': data}
|
||||||
|
|
||||||
def get_supported_batches(self):
|
def get_supported_batches(self):
|
||||||
return {
|
return {
|
||||||
'labels': 'rattail.batch.labels:LabelBatchHandler',
|
'labels': 'rattail.batch.labels:LabelBatchHandler',
|
||||||
|
@ -479,6 +519,11 @@ class ProductsView(MasterView):
|
||||||
config.add_view(cls, attr='make_batch', route_name='products.create_batch',
|
config.add_view(cls, attr='make_batch', route_name='products.create_batch',
|
||||||
renderer='/products/batch.mako', permission='batches.create')
|
renderer='/products/batch.mako', permission='batches.create')
|
||||||
|
|
||||||
|
# search (by upc)
|
||||||
|
config.add_route('products.search', '/products/search')
|
||||||
|
config.add_view(cls, attr='search', route_name='products.search',
|
||||||
|
renderer='json', permission='products.view')
|
||||||
|
|
||||||
cls._defaults(config)
|
cls._defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
@ -535,34 +580,6 @@ class ProductsAutocomplete(AutocompleteView):
|
||||||
return product.full_description
|
return product.full_description
|
||||||
|
|
||||||
|
|
||||||
def products_search(request):
|
|
||||||
"""
|
|
||||||
Locate a product(s) by UPC.
|
|
||||||
|
|
||||||
Eventually this should be more generic, or at least offer more fields for
|
|
||||||
search. For now it operates only on the ``Product.upc`` field.
|
|
||||||
"""
|
|
||||||
product = None
|
|
||||||
upc = request.GET.get('upc', '').strip()
|
|
||||||
upc = re.sub(r'\D', '', upc)
|
|
||||||
if upc:
|
|
||||||
product = api.get_product_by_upc(Session(), upc)
|
|
||||||
if not product:
|
|
||||||
# Try again, assuming caller did not include check digit.
|
|
||||||
upc = GPC(upc, calc_check_digit='upc')
|
|
||||||
product = api.get_product_by_upc(Session(), upc)
|
|
||||||
if product:
|
|
||||||
if product.deleted and not request.has_perm('products.view_deleted'):
|
|
||||||
product = None
|
|
||||||
else:
|
|
||||||
product = {
|
|
||||||
'uuid': product.uuid,
|
|
||||||
'upc': unicode(product.upc or ''),
|
|
||||||
'full_description': product.full_description,
|
|
||||||
}
|
|
||||||
return {'product': product}
|
|
||||||
|
|
||||||
|
|
||||||
def print_labels(request):
|
def print_labels(request):
|
||||||
profile = request.params.get('profile')
|
profile = request.params.get('profile')
|
||||||
profile = Session.query(model.LabelProfile).get(profile) if profile else None
|
profile = Session.query(model.LabelProfile).get(profile) if profile else None
|
||||||
|
@ -600,9 +617,5 @@ def includeme(config):
|
||||||
config.add_view(print_labels, route_name='products.print_labels',
|
config.add_view(print_labels, route_name='products.print_labels',
|
||||||
renderer='json', permission='products.print_labels')
|
renderer='json', permission='products.print_labels')
|
||||||
|
|
||||||
config.add_route('products.search', '/products/search')
|
|
||||||
config.add_view(products_search, route_name='products.search',
|
|
||||||
renderer='json', permission='products.list')
|
|
||||||
|
|
||||||
ProductsView.defaults(config)
|
ProductsView.defaults(config)
|
||||||
version_defaults(config, ProductVersionView, 'product')
|
version_defaults(config, ProductVersionView, 'product')
|
||||||
|
|
|
@ -26,9 +26,12 @@ Views for purchase order batches
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
|
||||||
from rattail import enum
|
from rattail import enum, pod
|
||||||
from rattail.db import model, api
|
from rattail.db import model, api
|
||||||
from rattail.gpc import GPC
|
from rattail.gpc import GPC
|
||||||
from rattail.time import localtime
|
from rattail.time import localtime
|
||||||
|
@ -36,6 +39,7 @@ from rattail.core import Object
|
||||||
from rattail.util import OrderedDict
|
from rattail.util import OrderedDict
|
||||||
|
|
||||||
import formalchemy as fa
|
import formalchemy as fa
|
||||||
|
import formencode as fe
|
||||||
from pyramid import httpexceptions
|
from pyramid import httpexceptions
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms
|
||||||
|
@ -43,6 +47,21 @@ from tailbone.db import Session
|
||||||
from tailbone.views.batch import BatchMasterView
|
from tailbone.views.batch import BatchMasterView
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ReceivingForm(forms.Schema):
|
||||||
|
allow_extra_fields = True
|
||||||
|
filter_extra_fields = True
|
||||||
|
mode = fe.validators.OneOf([
|
||||||
|
'received',
|
||||||
|
# 'damaged', 'expired', 'mispick',
|
||||||
|
])
|
||||||
|
product = forms.validators.ValidProduct()
|
||||||
|
cases = fe.validators.Int()
|
||||||
|
units = fe.validators.Int()
|
||||||
|
|
||||||
|
|
||||||
class PurchaseBatchView(BatchMasterView):
|
class PurchaseBatchView(BatchMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for purchase order batches.
|
Master view for purchase order batches.
|
||||||
|
@ -578,6 +597,95 @@ class PurchaseBatchView(BatchMasterView):
|
||||||
'batch_po_total': '${:0,.2f}'.format(batch.po_total),
|
'batch_po_total': '${:0,.2f}'.format(batch.po_total),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def receiving_form(self):
|
||||||
|
"""
|
||||||
|
Workflow view for receiving items on a purchase batch.
|
||||||
|
"""
|
||||||
|
batch = self.get_instance()
|
||||||
|
if batch.executed:
|
||||||
|
return self.redirect(self.get_action_url('view', batch))
|
||||||
|
|
||||||
|
form = forms.SimpleForm(self.request, schema=ReceivingForm)
|
||||||
|
if form.validate():
|
||||||
|
assert form.data['mode'] == 'received' # TODO
|
||||||
|
|
||||||
|
product = form.data['product']
|
||||||
|
rows = [row for row in batch.active_rows() if row.product is product]
|
||||||
|
if rows:
|
||||||
|
if len(rows) > 1:
|
||||||
|
log.warning("found {} matching rows in batch {} for product: {}".format(
|
||||||
|
len(rows), batch.id_str, product.upc.pretty()))
|
||||||
|
row = rows[0]
|
||||||
|
else:
|
||||||
|
row = model.PurchaseBatchRow()
|
||||||
|
row.product = product
|
||||||
|
|
||||||
|
if form.data['cases']:
|
||||||
|
row.cases_received = (row.cases_received or 0) + form.data['cases']
|
||||||
|
if form.data['units']:
|
||||||
|
row.units_received = (row.units_received or 0) + form.data['units']
|
||||||
|
|
||||||
|
if not row.uuid:
|
||||||
|
batch.add_row(row)
|
||||||
|
self.handler.refresh_row(row)
|
||||||
|
|
||||||
|
self.request.session.flash("({}) {} cases, {} units: {} {}".format(
|
||||||
|
form.data['mode'], form.data['cases'] or 0, form.data['units'] or 0,
|
||||||
|
product.upc.pretty(), product))
|
||||||
|
return self.redirect(self.request.current_route_url())
|
||||||
|
|
||||||
|
title = self.get_instance_title(batch)
|
||||||
|
return self.render_to_response('receive_form', {
|
||||||
|
'batch': batch,
|
||||||
|
'instance': batch,
|
||||||
|
'instance_title': title,
|
||||||
|
'index_title': "{}: {}".format(self.get_model_title(), title),
|
||||||
|
'index_url': self.get_action_url('view', batch),
|
||||||
|
'vendor': batch.vendor,
|
||||||
|
'form': forms.FormRenderer(form),
|
||||||
|
})
|
||||||
|
|
||||||
|
def receiving_lookup(self):
|
||||||
|
"""
|
||||||
|
Try to locate a product by UPC, and validate it in the context of
|
||||||
|
current batch, returning some data for client JS.
|
||||||
|
"""
|
||||||
|
batch = self.get_instance()
|
||||||
|
if batch.executed:
|
||||||
|
return {
|
||||||
|
'error': "Current batch has already been executed",
|
||||||
|
'redirect': self.get_action_url('view', batch),
|
||||||
|
}
|
||||||
|
data = None
|
||||||
|
upc = self.request.GET.get('upc', '').strip()
|
||||||
|
upc = re.sub(r'\D', '', upc)
|
||||||
|
if upc:
|
||||||
|
product = api.get_product_by_upc(Session(), upc)
|
||||||
|
if not product:
|
||||||
|
# Try again, assuming caller did not include check digit.
|
||||||
|
upc = GPC(upc, calc_check_digit='upc')
|
||||||
|
product = api.get_product_by_upc(Session(), upc)
|
||||||
|
if product and (not product.deleted or self.request.has_perm('products.view_deleted')):
|
||||||
|
data = {
|
||||||
|
'uuid': product.uuid,
|
||||||
|
'upc': unicode(product.upc),
|
||||||
|
'upc_pretty': product.upc.pretty(),
|
||||||
|
'full_description': product.full_description,
|
||||||
|
'image_url': pod.get_image_url(self.rattail_config, product.upc),
|
||||||
|
}
|
||||||
|
cost = product.cost_for_vendor(batch.vendor)
|
||||||
|
if cost:
|
||||||
|
data['cost_found'] = True
|
||||||
|
if int(cost.case_size) == cost.case_size:
|
||||||
|
data['cost_case_size'] = int(cost.case_size)
|
||||||
|
else:
|
||||||
|
data['cost_case_size'] = '{:0.4f}'.format(cost.case_size)
|
||||||
|
else:
|
||||||
|
data['cost_found'] = False
|
||||||
|
data['found_in_batch'] = product in [row.product for row in batch.active_rows()]
|
||||||
|
|
||||||
|
return {'product': data}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config):
|
def defaults(cls, config):
|
||||||
route_prefix = cls.get_route_prefix()
|
route_prefix = cls.get_route_prefix()
|
||||||
|
@ -595,7 +703,7 @@ class PurchaseBatchView(BatchMasterView):
|
||||||
cls._batch_defaults(config)
|
cls._batch_defaults(config)
|
||||||
cls._defaults(config)
|
cls._defaults(config)
|
||||||
|
|
||||||
# order form
|
# ordering form
|
||||||
config.add_tailbone_permission(permission_prefix, '{}.order_form'.format(permission_prefix),
|
config.add_tailbone_permission(permission_prefix, '{}.order_form'.format(permission_prefix),
|
||||||
"Edit new {} in Order Form mode".format(model_title))
|
"Edit new {} in Order Form mode".format(model_title))
|
||||||
config.add_route('{}.order_form'.format(route_prefix), '{}/{{{}}}/order-form'.format(url_prefix, model_key))
|
config.add_route('{}.order_form'.format(route_prefix), '{}/{{{}}}/order-form'.format(url_prefix, model_key))
|
||||||
|
@ -605,6 +713,16 @@ class PurchaseBatchView(BatchMasterView):
|
||||||
config.add_view(cls, attr='order_form_update', route_name='{}.order_form_update'.format(route_prefix),
|
config.add_view(cls, attr='order_form_update', route_name='{}.order_form_update'.format(route_prefix),
|
||||||
renderer='json', permission='{}.order_form'.format(permission_prefix))
|
renderer='json', permission='{}.order_form'.format(permission_prefix))
|
||||||
|
|
||||||
|
# receiving form, lookup
|
||||||
|
config.add_tailbone_permission(permission_prefix, '{}.receiving_form'.format(permission_prefix),
|
||||||
|
"Edit 'receiving' {} in Receiving Form mode".format(model_title))
|
||||||
|
config.add_route('{}.receiving_form'.format(route_prefix), '{}/{{{}}}/receiving-form'.format(url_prefix, model_key))
|
||||||
|
config.add_view(cls, attr='receiving_form', route_name='{}.receiving_form'.format(route_prefix),
|
||||||
|
permission='{}.receiving_form'.format(permission_prefix))
|
||||||
|
config.add_route('{}.receiving_lookup'.format(route_prefix), '{}/{{{}}}/receiving-form/lookup'.format(url_prefix, model_key))
|
||||||
|
config.add_view(cls, attr='receiving_lookup', route_name='{}.receiving_lookup'.format(route_prefix),
|
||||||
|
renderer='json', permission='{}.receiving_form'.format(permission_prefix))
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
PurchaseBatchView.defaults(config)
|
PurchaseBatchView.defaults(config)
|
||||||
|
|
Loading…
Reference in a new issue