Overhaul desktop views for receiving, for efficiency
still could use even more i'm sure, but this takes advantage of buefy to add dialogs etc. from the "view receiving batch row" page. this batch no longer allows direct edit of rows but that's hopefully for the better.
This commit is contained in:
parent
2f676774e9
commit
340a177a29
14 changed files with 1014 additions and 157 deletions
|
@ -115,7 +115,9 @@ class BatchMasterView(MasterView):
|
|||
|
||||
def __init__(self, request):
|
||||
super(BatchMasterView, self).__init__(request)
|
||||
self.handler = self.get_handler()
|
||||
self.batch_handler = self.get_handler()
|
||||
# TODO: deprecate / remove this (?)
|
||||
self.handler = self.batch_handler
|
||||
|
||||
@classmethod
|
||||
def get_handler_factory(cls, rattail_config):
|
||||
|
@ -1149,18 +1151,27 @@ class BatchMasterView(MasterView):
|
|||
"""
|
||||
Batch rows are editable only until batch is complete or executed.
|
||||
"""
|
||||
if not (self.rows_editable or self.rows_editable_but_not_directly):
|
||||
return False
|
||||
|
||||
batch = self.get_parent(row)
|
||||
return self.rows_editable and not batch.executed and not batch.complete
|
||||
if batch.complete or batch.executed:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def row_deletable(self, row):
|
||||
"""
|
||||
Batch rows are deletable only until batch is complete or executed.
|
||||
"""
|
||||
if self.rows_deletable:
|
||||
batch = self.get_parent(row)
|
||||
if not batch.executed and not batch.complete:
|
||||
return True
|
||||
return False
|
||||
if not self.rows_deletable:
|
||||
return False
|
||||
|
||||
batch = self.get_parent(row)
|
||||
if batch.complete or batch.executed:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def template_kwargs_view_row(self, **kwargs):
|
||||
kwargs['batch_model_title'] = kwargs['parent_model_title']
|
||||
|
|
|
@ -166,6 +166,7 @@ class MasterView(View):
|
|||
rows_viewable = True
|
||||
rows_creatable = False
|
||||
rows_editable = False
|
||||
rows_editable_but_not_directly = False
|
||||
rows_deletable = False
|
||||
rows_deletable_speedbump = True
|
||||
rows_bulk_deletable = False
|
||||
|
@ -3852,6 +3853,7 @@ class MasterView(View):
|
|||
return self.render_to_response('edit_row', {
|
||||
'instance': row,
|
||||
'row_parent': parent,
|
||||
'parent_model_title': self.get_model_title(),
|
||||
'parent_title': self.get_instance_title(parent),
|
||||
'parent_url': self.get_action_url('view', parent),
|
||||
'parent_instance': parent,
|
||||
|
@ -3884,6 +3886,8 @@ class MasterView(View):
|
|||
considered "deletable". Returns ``True`` by default; override as
|
||||
necessary.
|
||||
"""
|
||||
if not self.rows_deletable:
|
||||
return False
|
||||
return True
|
||||
|
||||
def delete_row_object(self, row):
|
||||
|
@ -4099,6 +4103,7 @@ class MasterView(View):
|
|||
config_title = cls.get_config_title()
|
||||
if cls.has_rows:
|
||||
row_model_title = cls.get_row_model_title()
|
||||
row_model_title_plural = cls.get_row_model_title_plural()
|
||||
|
||||
config.add_tailbone_permission_group(permission_prefix, model_title_plural, overwrite=False)
|
||||
|
||||
|
@ -4386,9 +4391,10 @@ class MasterView(View):
|
|||
|
||||
# edit row
|
||||
if cls.has_rows:
|
||||
if cls.rows_editable:
|
||||
if cls.rows_editable or cls.rows_editable_but_not_directly:
|
||||
config.add_tailbone_permission(permission_prefix, '{}.edit_row'.format(permission_prefix),
|
||||
"Edit individual {} rows".format(model_title))
|
||||
"Edit individual {}".format(row_model_title_plural))
|
||||
if cls.rows_editable:
|
||||
config.add_route('{}.edit_row'.format(route_prefix), '{}/{{uuid}}/rows/{{row_uuid}}/edit'.format(url_prefix))
|
||||
config.add_view(cls, attr='edit_row', route_name='{}.edit_row'.format(route_prefix),
|
||||
permission='{}.edit_row'.format(permission_prefix))
|
||||
|
@ -4397,7 +4403,7 @@ class MasterView(View):
|
|||
if cls.has_rows:
|
||||
if cls.rows_deletable:
|
||||
config.add_tailbone_permission(permission_prefix, '{}.delete_row'.format(permission_prefix),
|
||||
"Delete individual {} rows".format(model_title))
|
||||
"Delete individual {}".format(row_model_title_plural))
|
||||
config.add_route('{}.delete_row'.format(route_prefix), '{}/{{uuid}}/rows/{{row_uuid}}/delete'.format(url_prefix))
|
||||
config.add_view(cls, attr='delete_row', route_name='{}.delete_row'.format(route_prefix),
|
||||
permission='{}.delete_row'.format(permission_prefix))
|
||||
|
|
|
@ -99,8 +99,10 @@ class PurchasingBatchView(BatchMasterView):
|
|||
'upc': "UPC",
|
||||
'item_id': "Item ID",
|
||||
'brand_name': "Brand",
|
||||
'case_quantity': "Case Size",
|
||||
'po_line_number': "PO Line Number",
|
||||
'po_unit_cost': "PO Unit Cost",
|
||||
'po_case_size': "PO Case Size",
|
||||
'po_total': "PO Total",
|
||||
}
|
||||
|
||||
|
@ -144,6 +146,9 @@ class PurchasingBatchView(BatchMasterView):
|
|||
'mispick',
|
||||
'cases_mispick',
|
||||
'units_mispick',
|
||||
'missing',
|
||||
'cases_missing',
|
||||
'units_missing',
|
||||
'po_line_number',
|
||||
'po_unit_cost',
|
||||
'po_total',
|
||||
|
@ -710,8 +715,11 @@ class PurchasingBatchView(BatchMasterView):
|
|||
f.set_renderer('damaged', self.render_row_quantity)
|
||||
f.set_renderer('expired', self.render_row_quantity)
|
||||
f.set_renderer('mispick', self.render_row_quantity)
|
||||
f.set_renderer('missing', self.render_row_quantity)
|
||||
|
||||
f.set_type('case_quantity', 'quantity')
|
||||
f.set_type('po_case_size', 'quantity')
|
||||
f.set_type('invoice_case_size', 'quantity')
|
||||
f.set_type('cases_ordered', 'quantity')
|
||||
f.set_type('units_ordered', 'quantity')
|
||||
f.set_type('cases_shipped', 'quantity')
|
||||
|
@ -724,6 +732,8 @@ class PurchasingBatchView(BatchMasterView):
|
|||
f.set_type('units_expired', 'quantity')
|
||||
f.set_type('cases_mispick', 'quantity')
|
||||
f.set_type('units_mispick', 'quantity')
|
||||
f.set_type('cases_missing', 'quantity')
|
||||
f.set_type('units_missing', 'quantity')
|
||||
|
||||
# currency fields
|
||||
# nb. we only show "total" fields as currency, but not case or
|
||||
|
@ -746,7 +756,8 @@ class PurchasingBatchView(BatchMasterView):
|
|||
|
||||
# credits
|
||||
f.set_readonly('credits')
|
||||
f.set_renderer('credits', self.render_row_credits)
|
||||
if self.viewing:
|
||||
f.set_renderer('credits', self.render_row_credits)
|
||||
|
||||
if self.creating:
|
||||
f.remove_fields(
|
||||
|
@ -786,36 +797,58 @@ class PurchasingBatchView(BatchMasterView):
|
|||
app = self.get_rattail_app()
|
||||
cases = getattr(row, 'cases_{}'.format(field))
|
||||
units = getattr(row, 'units_{}'.format(field))
|
||||
if cases and units:
|
||||
return "{} cases + {} units".format(app.render_quantity(cases),
|
||||
app.render_quantity(units))
|
||||
if cases and not units:
|
||||
return "{} cases".format(app.render_quantity(cases))
|
||||
if units and not cases:
|
||||
return "{} units".format(app.render_quantity(units))
|
||||
|
||||
def render_row_credits(self, row, field):
|
||||
if not row.credits:
|
||||
return ""
|
||||
return app.render_cases_units(cases, units)
|
||||
|
||||
def make_row_credits_grid(self, row):
|
||||
use_buefy = self.get_use_buefy()
|
||||
route_prefix = self.get_route_prefix()
|
||||
columns = [
|
||||
'credit_type',
|
||||
'cases_shorted',
|
||||
'units_shorted',
|
||||
'credit_total',
|
||||
]
|
||||
g = grids.Grid(
|
||||
factory = self.get_grid_factory()
|
||||
|
||||
g = factory(
|
||||
key='{}.row_credits'.format(route_prefix),
|
||||
data=row.credits,
|
||||
columns=columns,
|
||||
labels={'credit_type': "Type",
|
||||
'cases_shorted': "Cases",
|
||||
'units_shorted': "Units"})
|
||||
data=[] if use_buefy else row.credits,
|
||||
columns=[
|
||||
'credit_type',
|
||||
# 'cases_shorted',
|
||||
# 'units_shorted',
|
||||
'shorted',
|
||||
'credit_total',
|
||||
'expiration_date',
|
||||
# 'mispick_upc',
|
||||
# 'mispick_brand_name',
|
||||
# 'mispick_description',
|
||||
# 'mispick_size',
|
||||
],
|
||||
labels={
|
||||
'credit_type': "Type",
|
||||
'cases_shorted': "Cases",
|
||||
'units_shorted': "Units",
|
||||
'shorted': "Quantity",
|
||||
'credit_total': "Total",
|
||||
'mispick_upc': "Mispick UPC",
|
||||
'mispick_brand_name': "MP Brand",
|
||||
'mispick_description': "MP Description",
|
||||
'mispick_size': "MP Size",
|
||||
})
|
||||
|
||||
g.set_type('cases_shorted', 'quantity')
|
||||
g.set_type('units_shorted', 'quantity')
|
||||
g.set_type('credit_total', 'currency')
|
||||
return HTML.literal(g.render_grid())
|
||||
|
||||
return g
|
||||
|
||||
def render_row_credits(self, row, field):
|
||||
use_buefy = self.get_use_buefy()
|
||||
if not use_buefy and not row.credits:
|
||||
return
|
||||
|
||||
g = self.make_row_credits_grid(row)
|
||||
|
||||
if use_buefy:
|
||||
return HTML.literal(
|
||||
g.render_buefy_table_element(data_prop='rowData.credits'))
|
||||
else:
|
||||
return HTML.literal(g.render_grid())
|
||||
|
||||
# def item_lookup(self, value, field=None):
|
||||
# """
|
||||
|
|
|
@ -51,6 +51,21 @@ from tailbone.views.purchasing import PurchasingBatchView
|
|||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
POSSIBLE_RECEIVING_MODES = [
|
||||
'received',
|
||||
'damaged',
|
||||
'expired',
|
||||
# 'mispick',
|
||||
'missing',
|
||||
]
|
||||
|
||||
POSSIBLE_CREDIT_TYPES = [
|
||||
'damaged',
|
||||
'expired',
|
||||
# 'mispick',
|
||||
'missing',
|
||||
]
|
||||
|
||||
|
||||
class ReceivingBatchView(PurchasingBatchView):
|
||||
"""
|
||||
|
@ -63,7 +78,9 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
index_title = "Receiving"
|
||||
downloadable = True
|
||||
bulk_deletable = True
|
||||
rows_editable = True
|
||||
rows_editable = False
|
||||
rows_editable_but_not_directly = True
|
||||
rows_deletable = True
|
||||
|
||||
default_uom_is_case = True
|
||||
|
||||
|
@ -181,13 +198,18 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
'mispick',
|
||||
'cases_mispick',
|
||||
'units_mispick',
|
||||
'missing',
|
||||
'cases_missing',
|
||||
'units_missing',
|
||||
'catalog_unit_cost',
|
||||
'po_line_number',
|
||||
'po_unit_cost',
|
||||
'po_case_size',
|
||||
'po_total',
|
||||
'invoice_line_number',
|
||||
'invoice_unit_cost',
|
||||
'invoice_cost_confirmed',
|
||||
'invoice_case_size',
|
||||
'invoice_total',
|
||||
'invoice_total_calculated',
|
||||
'status_code',
|
||||
|
@ -322,17 +344,14 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
return self.render_to_response('create', context)
|
||||
|
||||
def row_deletable(self, row):
|
||||
|
||||
# first run it through the normal logic, if that doesn't like
|
||||
# it then we won't either
|
||||
if not super(ReceivingBatchView, self).row_deletable(row):
|
||||
return False
|
||||
|
||||
batch = row.batch
|
||||
|
||||
# don't allow if master view has disabled that entirely
|
||||
if not self.rows_deletable:
|
||||
return False
|
||||
|
||||
# can never delete rows for complete/executed batches
|
||||
# TODO: not so sure about the 'complete' part though..?
|
||||
if batch.executed or batch.complete:
|
||||
return False
|
||||
|
||||
# can always delete rows from truck dump parent
|
||||
if batch.is_truck_dump_parent():
|
||||
return True
|
||||
|
@ -362,7 +381,7 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
super(ReceivingBatchView, self).configure_form(f)
|
||||
model = self.model
|
||||
batch = f.model_instance
|
||||
allow_truck_dump = self.handler.allow_truck_dump_receiving()
|
||||
allow_truck_dump = self.batch_handler.allow_truck_dump_receiving()
|
||||
workflow = self.request.matchdict.get('workflow_key')
|
||||
route_prefix = self.get_route_prefix()
|
||||
use_buefy = self.get_use_buefy()
|
||||
|
@ -472,9 +491,9 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
and self.purchase_order_fieldname == 'purchase'):
|
||||
if use_buefy:
|
||||
f.replace('purchase', 'purchase_uuid')
|
||||
purchases = self.handler.get_eligible_purchases(
|
||||
purchases = self.batch_handler.get_eligible_purchases(
|
||||
vendor, self.enum.PURCHASE_BATCH_MODE_RECEIVING)
|
||||
values = [(p.uuid, self.handler.render_eligible_purchase(p))
|
||||
values = [(p.uuid, 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")
|
||||
|
@ -497,12 +516,11 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
f.remove('invoice_total_calculated')
|
||||
|
||||
# hide all invoice fields if batch does not have invoice file
|
||||
if not self.creating and not self.handler.has_invoice_file(batch):
|
||||
if not self.creating and not self.batch_handler.has_invoice_file(batch):
|
||||
f.remove('invoice_file',
|
||||
'invoice_date',
|
||||
'invoice_number',
|
||||
'invoice_total',
|
||||
'invoice_total_calculated')
|
||||
'invoice_total')
|
||||
|
||||
# receiving_complete
|
||||
if self.creating:
|
||||
|
@ -517,9 +535,12 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
'invoice_parser_key')
|
||||
|
||||
elif workflow == 'from_invoice':
|
||||
f.remove('truck_dump_batch_uuid')
|
||||
f.set_required('invoice_file')
|
||||
f.set_required('invoice_parser_key')
|
||||
f.remove('truck_dump_batch_uuid',
|
||||
'po_number',
|
||||
'invoice_date',
|
||||
'invoice_number')
|
||||
|
||||
elif workflow == 'from_po':
|
||||
f.remove('truck_dump_batch_uuid',
|
||||
|
@ -531,9 +552,13 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
'invoice_number')
|
||||
|
||||
elif workflow == 'from_po_with_invoice':
|
||||
f.remove('truck_dump_batch_uuid')
|
||||
f.set_required('invoice_file')
|
||||
f.set_required('invoice_parser_key')
|
||||
f.remove('truck_dump_batch_uuid',
|
||||
'date_ordered',
|
||||
'po_number',
|
||||
'invoice_date',
|
||||
'invoice_number')
|
||||
|
||||
elif workflow == 'truck_dump_children_first':
|
||||
f.remove('truck_dump_batch_uuid',
|
||||
|
@ -614,16 +639,92 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
raise NotImplementedError
|
||||
return kwargs
|
||||
|
||||
def make_po_vs_invoice_breakdown(self, batch):
|
||||
"""
|
||||
Returns a simple breakdown as list of 2-tuples, each of which
|
||||
has the display title as first member, and number of rows as
|
||||
second member.
|
||||
"""
|
||||
grouped = {}
|
||||
labels = OrderedDict([
|
||||
('both', "Found in both PO and Invoice"),
|
||||
('po_not_invoice', "Found in PO but not Invoice"),
|
||||
('invoice_not_po', "Found in Invoice but not PO"),
|
||||
('neither', "Not found in PO nor Invoice"),
|
||||
])
|
||||
|
||||
for row in batch.active_rows():
|
||||
if row.po_line_number and not row.invoice_line_number:
|
||||
grouped.setdefault('po_not_invoice', []).append(row)
|
||||
elif row.invoice_line_number and not row.po_line_number:
|
||||
grouped.setdefault('invoice_not_po', []).append(row)
|
||||
elif row.po_line_number and row.invoice_line_number:
|
||||
grouped.setdefault('both', []).append(row)
|
||||
else:
|
||||
grouped.setdefault('neither', []).append(row)
|
||||
|
||||
breakdown = []
|
||||
|
||||
for key, label in labels.items():
|
||||
if key in grouped:
|
||||
breakdown.append({
|
||||
'title': label,
|
||||
'count': len(grouped[key]),
|
||||
})
|
||||
|
||||
return breakdown
|
||||
|
||||
def template_kwargs_view(self, **kwargs):
|
||||
kwargs = super(ReceivingBatchView, self).template_kwargs_view(**kwargs)
|
||||
batch = kwargs['instance']
|
||||
|
||||
if self.handler.has_purchase_order(batch) and self.handler.has_invoice_file(batch):
|
||||
breakdown = self.make_po_vs_invoice_breakdown(batch)
|
||||
|
||||
factory = self.get_grid_factory()
|
||||
kwargs['po_vs_invoice_breakdown_grid'] = factory(
|
||||
'batch_po_vs_invoice_breakdown',
|
||||
data=breakdown,
|
||||
columns=['title', 'count'])
|
||||
|
||||
return kwargs
|
||||
|
||||
def get_context_credits(self, row):
|
||||
app = self.get_rattail_app()
|
||||
credits_data = []
|
||||
for credit in row.credits:
|
||||
credits_data.append({
|
||||
'uuid': credit.uuid,
|
||||
'credit_type': credit.credit_type,
|
||||
'expiration_date': six.text_type(credit.expiration_date) if credit.expiration_date else None,
|
||||
'cases_shorted': app.render_quantity(credit.cases_shorted),
|
||||
'units_shorted': app.render_quantity(credit.units_shorted),
|
||||
'shorted': app.render_cases_units(credit.cases_shorted,
|
||||
credit.units_shorted),
|
||||
'credit_total': app.render_currency(credit.credit_total),
|
||||
'mispick_upc': '-',
|
||||
'mispick_brand_name': '-',
|
||||
'mispick_description': '-',
|
||||
'mispick_size': '-',
|
||||
})
|
||||
return credits_data
|
||||
|
||||
def template_kwargs_view_row(self, **kwargs):
|
||||
kwargs = super(ReceivingBatchView, self).template_kwargs_view_row(**kwargs)
|
||||
use_buefy = self.get_use_buefy()
|
||||
app = self.get_rattail_app()
|
||||
handler = app.get_products_handler()
|
||||
products_handler = app.get_products_handler()
|
||||
row = kwargs['instance']
|
||||
|
||||
if row.product:
|
||||
kwargs['image_url'] = handler.get_image_url(row.product)
|
||||
kwargs['image_url'] = products_handler.get_image_url(row.product)
|
||||
elif row.upc:
|
||||
kwargs['image_url'] = handler.get_image_url(upc=row.upc)
|
||||
kwargs['image_url'] = products_handler.get_image_url(upc=row.upc)
|
||||
|
||||
if use_buefy:
|
||||
kwargs['row_context'] = self.get_context_row(row)
|
||||
kwargs['possible_receiving_modes'] = POSSIBLE_RECEIVING_MODES
|
||||
kwargs['possible_credit_types'] = POSSIBLE_CREDIT_TYPES
|
||||
|
||||
return kwargs
|
||||
|
||||
|
@ -849,6 +950,24 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
if row.product and row.product.is_pack_item():
|
||||
return self.get_row_action_url('transform_unit', row)
|
||||
|
||||
def make_row_credits_grid(self, row):
|
||||
|
||||
# first make grid like normal
|
||||
g = super(ReceivingBatchView, self).make_row_credits_grid(row)
|
||||
|
||||
if (self.get_use_buefy()
|
||||
and self.has_perm('edit_row')
|
||||
and self.row_editable(row)):
|
||||
|
||||
# add the Un-Declare action
|
||||
g.main_actions.append(self.make_action(
|
||||
'remove', label="Un-Declare",
|
||||
url='#', icon='trash',
|
||||
link_class='has-text-danger',
|
||||
click_handler='removeCreditInit(props.row)'))
|
||||
|
||||
return g
|
||||
|
||||
def vuejs_convert_quantity(self, cstruct):
|
||||
result = dict(cstruct)
|
||||
if result['cases'] is colander.null:
|
||||
|
@ -872,6 +991,55 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
self.viewing = True
|
||||
use_buefy = self.get_use_buefy()
|
||||
row = self.get_row_instance()
|
||||
|
||||
# things are a bit different now w/ buefy support..
|
||||
if use_buefy:
|
||||
|
||||
# don't even bother showing this page if that's all the
|
||||
# request was about
|
||||
if self.request.method == 'GET':
|
||||
return self.redirect(self.get_row_action_url('view', row))
|
||||
|
||||
# make sure edit is allowed
|
||||
if not (self.has_perm('edit_row') and self.row_editable(row)):
|
||||
raise self.forbidden()
|
||||
|
||||
# check for JSON POST, which is submitted via AJAX from
|
||||
# the "view row" page
|
||||
if self.request.method == 'POST' and not self.request.POST:
|
||||
data = self.request.json_body
|
||||
kwargs = dict(data)
|
||||
|
||||
# TODO: for some reason quantities can come through as strings?
|
||||
cases = kwargs['quantity']['cases']
|
||||
if cases is not None:
|
||||
if cases == '':
|
||||
cases = None
|
||||
else:
|
||||
cases = decimal.Decimal(cases)
|
||||
kwargs['cases'] = cases
|
||||
units = kwargs['quantity']['units']
|
||||
if units is not None:
|
||||
if units == '':
|
||||
units = None
|
||||
else:
|
||||
units = decimal.Decimal(units)
|
||||
kwargs['units'] = units
|
||||
del kwargs['quantity']
|
||||
|
||||
# handler takes care of the receiving logic for us
|
||||
try:
|
||||
self.batch_handler.receive_row(row, **kwargs)
|
||||
|
||||
except Exception as error:
|
||||
return self.json_response({'error': six.text_type(error)})
|
||||
|
||||
self.Session.flush()
|
||||
self.Session.refresh(row)
|
||||
return self.json_response({
|
||||
'ok': True,
|
||||
'row': self.get_context_row(row)})
|
||||
|
||||
batch = row.batch
|
||||
permission_prefix = self.get_permission_prefix()
|
||||
possible_modes = [
|
||||
|
@ -1024,11 +1192,59 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
"""
|
||||
use_buefy = self.get_use_buefy()
|
||||
row = self.get_row_instance()
|
||||
|
||||
# things are a bit different now w/ buefy support..
|
||||
if use_buefy:
|
||||
|
||||
# don't even bother showing this page if that's all the
|
||||
# request was about
|
||||
if self.request.method == 'GET':
|
||||
return self.redirect(self.get_row_action_url('view', row))
|
||||
|
||||
# make sure edit is allowed
|
||||
if not (self.has_perm('edit_row') and self.row_editable(row)):
|
||||
raise self.forbidden()
|
||||
|
||||
# check for JSON POST, which is submitted via AJAX from
|
||||
# the "view row" page
|
||||
if self.request.method == 'POST' and not self.request.POST:
|
||||
data = self.request.json_body
|
||||
kwargs = dict(data)
|
||||
|
||||
# TODO: for some reason quantities can come through as strings?
|
||||
if kwargs['cases'] is not None:
|
||||
if kwargs['cases'] == '':
|
||||
kwargs['cases'] = None
|
||||
else:
|
||||
kwargs['cases'] = decimal.Decimal(kwargs['cases'])
|
||||
if kwargs['units'] is not None:
|
||||
if kwargs['units'] == '':
|
||||
kwargs['units'] = None
|
||||
else:
|
||||
kwargs['units'] = decimal.Decimal(kwargs['units'])
|
||||
|
||||
try:
|
||||
result = self.handler.can_declare_credit(row, **kwargs)
|
||||
|
||||
except Exception as error:
|
||||
return self.json_response({'error': six.text_type(error)})
|
||||
|
||||
else:
|
||||
if result:
|
||||
self.handler.declare_credit(row, **kwargs)
|
||||
|
||||
else:
|
||||
return self.json_response({
|
||||
'error': "Handler says you can't declare that credit; "
|
||||
"not sure why"})
|
||||
|
||||
self.Session.flush()
|
||||
self.Session.refresh(row)
|
||||
return self.json_response({
|
||||
'ok': True,
|
||||
'row': self.get_context_row(row)})
|
||||
|
||||
batch = row.batch
|
||||
possible_credit_types = [
|
||||
'damaged',
|
||||
'expired',
|
||||
]
|
||||
context = {
|
||||
'row': row,
|
||||
'batch': batch,
|
||||
|
@ -1044,9 +1260,10 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
schema = DeclareCreditForm()
|
||||
form = forms.Form(schema=schema, request=self.request,
|
||||
use_buefy=use_buefy)
|
||||
form.cancel_url = self.get_row_action_url('view', row)
|
||||
|
||||
# credit_type
|
||||
values = [(m, m) for m in possible_credit_types]
|
||||
values = [(m, m) for m in POSSIBLE_CREDIT_TYPES]
|
||||
if use_buefy:
|
||||
widget = dfwidget.SelectWidget(values=values)
|
||||
else:
|
||||
|
@ -1085,6 +1302,54 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
context['parent_title'] = self.get_instance_title(batch)
|
||||
return self.render_to_response('declare_credit', context)
|
||||
|
||||
def undeclare_credit(self):
|
||||
"""
|
||||
View for un-declaring a credit, i.e. moving the credit amounts
|
||||
back into the "received" tally.
|
||||
"""
|
||||
model = self.model
|
||||
row = self.get_row_instance()
|
||||
data = self.request.json_body
|
||||
|
||||
# make sure edit is allowed
|
||||
if not (self.has_perm('edit_row') and self.row_editable(row)):
|
||||
raise self.forbidden()
|
||||
|
||||
# figure out which credit to un-declare
|
||||
credit = None
|
||||
uuid = data.get('uuid')
|
||||
if uuid:
|
||||
credit = self.Session.query(model.PurchaseBatchCredit).get(uuid)
|
||||
if not credit:
|
||||
return {'error': "Credit not found"}
|
||||
|
||||
# un-declare it
|
||||
self.batch_handler.undeclare_credit(row, credit)
|
||||
self.Session.flush()
|
||||
self.Session.refresh(row)
|
||||
|
||||
return {'ok': True,
|
||||
'row': self.get_context_row(row)}
|
||||
|
||||
def get_context_row(self, row):
|
||||
app = self.get_rattail_app()
|
||||
return {
|
||||
'sequence': row.sequence,
|
||||
'case_quantity': float(row.case_quantity) if row.case_quantity is not None else None,
|
||||
'ordered': self.render_row_quantity(row, 'ordered'),
|
||||
'shipped': self.render_row_quantity(row, 'shipped'),
|
||||
'received': self.render_row_quantity(row, 'received'),
|
||||
'cases_received': float(row.cases_received) if row.cases_received is not None else None,
|
||||
'units_received': float(row.units_received) if row.units_received is not None else None,
|
||||
'damaged': self.render_row_quantity(row, 'damaged'),
|
||||
'expired': self.render_row_quantity(row, 'expired'),
|
||||
'mispick': self.render_row_quantity(row, 'mispick'),
|
||||
'missing': self.render_row_quantity(row, 'missing'),
|
||||
'credits': self.get_context_credits(row),
|
||||
'invoice_total_calculated': app.render_currency(row.invoice_total_calculated),
|
||||
'status': row.STATUS[row.status_code],
|
||||
}
|
||||
|
||||
def transform_unit_row(self):
|
||||
"""
|
||||
View which transforms the given row, which is assumed to associate with
|
||||
|
@ -1593,6 +1858,14 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
config.add_view(cls, attr='declare_credit', route_name='{}.declare_credit'.format(route_prefix),
|
||||
permission='{}.edit_row'.format(permission_prefix))
|
||||
|
||||
# un-declare credit
|
||||
config.add_route('{}.undeclare_credit'.format(route_prefix),
|
||||
'{}/rows/{{row_uuid}}/undeclare-credit'.format(instance_url_prefix))
|
||||
config.add_view(cls, attr='undeclare_credit',
|
||||
route_name='{}.undeclare_credit'.format(route_prefix),
|
||||
permission='{}.edit_row'.format(permission_prefix),
|
||||
renderer='json')
|
||||
|
||||
# update row cost
|
||||
config.add_route('{}.update_row_cost'.format(route_prefix), '{}/update-row-cost'.format(instance_url_prefix))
|
||||
config.add_view(cls, attr='update_row_cost', route_name='{}.update_row_cost'.format(route_prefix),
|
||||
|
@ -1649,12 +1922,8 @@ class NewReceivingBatch(colander.Schema):
|
|||
class ReceiveRowForm(colander.MappingSchema):
|
||||
|
||||
mode = colander.SchemaNode(colander.String(),
|
||||
validator=colander.OneOf([
|
||||
'received',
|
||||
'damaged',
|
||||
'expired',
|
||||
# 'mispick',
|
||||
]))
|
||||
validator=colander.OneOf(
|
||||
POSSIBLE_RECEIVING_MODES))
|
||||
|
||||
quantity = forms.types.ProductQuantity()
|
||||
|
||||
|
@ -1677,11 +1946,8 @@ class ReceiveRowForm(colander.MappingSchema):
|
|||
class DeclareCreditForm(colander.MappingSchema):
|
||||
|
||||
credit_type = colander.SchemaNode(colander.String(),
|
||||
validator=colander.OneOf([
|
||||
'damaged',
|
||||
'expired',
|
||||
# 'mispick',
|
||||
]))
|
||||
validator=colander.OneOf(
|
||||
POSSIBLE_CREDIT_TYPES))
|
||||
|
||||
quantity = forms.types.ProductQuantity()
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue