# -*- coding: utf-8; -*- ################################################################################ # # Rattail -- Retail Software Framework # Copyright © 2010-2019 Lance Edgar # # This file is part of Rattail. # # Rattail is free software: you can redistribute it and/or modify it under the # terms of the GNU General Public License as published by the Free Software # Foundation, either version 3 of the License, or (at your option) any later # version. # # Rattail is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # Rattail. If not, see . # ################################################################################ """ Tailbone Web API - Receiving Batches """ from __future__ import unicode_literals, absolute_import import six import humanize from rattail.db import model from rattail.time import make_utc from tailbone.api.batch import APIBatchView, APIBatchRowView class ReceivingBatchViews(APIBatchView): model_class = model.PurchaseBatch default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler' route_prefix = 'receivingbatchviews' permission_prefix = 'receiving' collection_url_prefix = '/receiving-batches' object_url_prefix = '/receiving-batch' supports_toggle_complete = True def normalize(self, batch): data = super(ReceivingBatchViews, self).normalize(batch) data['vendor_uuid'] = batch.vendor.uuid data['vendor_display'] = six.text_type(batch.vendor) data['department_uuid'] = batch.department_uuid data['department_display'] = six.text_type(batch.department) if batch.department else None return data def get_purchase(self, uuid): return self.Session.query(model.Purchase).get(uuid) def create_object(self, data): data = dict(data) data['mode'] = self.enum.PURCHASE_BATCH_MODE_RECEIVING # if 'purchase_key' in data: # purchase = self.get_purchase(data['purchase_key']) # data['purchase'] = purchase batch = super(ReceivingBatchViews, self).create_object(data) return batch def collection_get(self): return self._collection_get() def collection_post(self): return self._collection_post() def get(self): return self._get() def eligible_purchases(self): uuid = self.request.params.get('vendor_uuid') vendor = self.Session.query(model.Vendor).get(uuid) if uuid else None if not vendor: return {'error': "Vendor not found"} purchases = self.handler.get_eligible_purchases( vendor, self.enum.PURCHASE_BATCH_MODE_RECEIVING) purchases = [self.normalize_eligible_purchase(p) for p in purchases] return {'purchases': purchases} def normalize_eligible_purchase(self, purchase): return { 'key': purchase.uuid, 'department_uuid': purchase.department_uuid, 'display': self.render_eligible_purchase(purchase), } 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, purchase.department or purchase.buyer) @classmethod def defaults(cls, config): cls._batch_defaults(config) cls._receiving_batch_defaults(config) @classmethod def _receiving_batch_defaults(cls, config): route_prefix = cls.get_route_prefix() permission_prefix = cls.get_permission_prefix() collection_url_prefix = cls.get_collection_url_prefix() # eligible purchases config.add_route('{}.eligible_purchases'.format(route_prefix), '{}/eligible-purchases'.format(collection_url_prefix), request_method='GET') config.add_view(cls, attr='eligible_purchases', route_name='{}.eligible_purchases'.format(route_prefix), permission='{}.create'.format(permission_prefix), renderer='json') class ReceivingBatchRowViews(APIBatchRowView): model_class = model.PurchaseBatchRow default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler' route_prefix = 'receiving.rows' permission_prefix = 'receiving' collection_url_prefix = '/receiving-batch-rows' object_url_prefix = '/receiving-batch-row' supports_quick_entry = True def make_filter_spec(self): filters = super(ReceivingBatchRowViews, self).make_filter_spec() if filters: # must translate certain convenience filters orig_filters, filters = filters, [] for filtr in orig_filters: # # is_received # # NOTE: this is only relevant for truck dump or "from scratch" # if filtr['field'] == 'is_received' and filtr['op'] == 'eq' and filtr['value'] is True: # filters.extend([ # {'or': [ # {'field': 'cases_received', 'op': '!=', 'value': 0}, # {'field': 'units_received', 'op': '!=', 'value': 0}, # ]}, # ]) # is_incomplete if filtr['field'] == 'is_incomplete' and filtr['op'] == 'eq' and filtr['value'] is True: # looking for any rows with "ordered" quantity, but where the # status does *not* signify a "settled" row so to speak # TODO: would be nice if we had a simple flag to leverage? filters.extend([ {'or': [ {'field': 'cases_ordered', 'op': '!=', 'value': 0}, {'field': 'units_ordered', 'op': '!=', 'value': 0}, ]}, {'field': 'status_code', 'op': 'not_in', 'value': [ model.PurchaseBatchRow.STATUS_OK, model.PurchaseBatchRow.STATUS_PRODUCT_NOT_FOUND, model.PurchaseBatchRow.STATUS_CASE_QUANTITY_DIFFERS, ]}, ]) # is_invalid elif filtr['field'] == 'is_invalid' and filtr['op'] == 'eq' and filtr['value'] is True: filters.extend([ {'field': 'status_code', 'op': 'in', 'value': [ model.PurchaseBatchRow.STATUS_PRODUCT_NOT_FOUND, model.PurchaseBatchRow.STATUS_COST_NOT_FOUND, model.PurchaseBatchRow.STATUS_CASE_QUANTITY_UNKNOWN, model.PurchaseBatchRow.STATUS_CASE_QUANTITY_DIFFERS, ]}, ]) # is_unexpected elif filtr['field'] == 'is_unexpected' and filtr['op'] == 'eq' and filtr['value'] is True: # looking for any rows which have "received" quantity but which # do *not* have any "ordered" quantity filters.extend([ {'and': [ {'or': [ {'field': 'cases_ordered', 'op': 'is_null'}, {'field': 'cases_ordered', 'op': '==', 'value': 0}, ]}, {'or': [ {'field': 'units_ordered', 'op': 'is_null'}, {'field': 'units_ordered', 'op': '==', 'value': 0}, ]}, {'or': [ {'field': 'cases_received', 'op': '!=', 'value': 0}, {'field': 'units_received', 'op': '!=', 'value': 0}, {'field': 'cases_damaged', 'op': '!=', 'value': 0}, {'field': 'units_damaged', 'op': '!=', 'value': 0}, {'field': 'cases_expired', 'op': '!=', 'value': 0}, {'field': 'units_expired', 'op': '!=', 'value': 0}, ]}, ]}, ]) # is_damaged elif filtr['field'] == 'is_damaged' and filtr['op'] == 'eq' and filtr['value'] is True: filters.extend([ {'or': [ {'field': 'cases_damaged', 'op': '!=', 'value': 0}, {'field': 'units_damaged', 'op': '!=', 'value': 0}, ]}, ]) # is_expired elif filtr['field'] == 'is_expired' and filtr['op'] == 'eq' and filtr['value'] is True: filters.extend([ {'or': [ {'field': 'cases_expired', 'op': '!=', 'value': 0}, {'field': 'units_expired', 'op': '!=', 'value': 0}, ]}, ]) else: # just some filter, use as-is filters.append(filtr) return filters def normalize(self, row): batch = row.batch data = super(ReceivingBatchRowViews, self).normalize(row) data['item_id'] = row.item_id data['upc'] = six.text_type(row.upc) data['upc_pretty'] = row.upc.pretty() if row.upc else None data['brand_name'] = row.brand_name data['description'] = row.description data['size'] = row.size data['full_description'] = row.product.full_description if row.product else row.description data['case_quantity'] = row.case_quantity data['unit_uom'] = 'EA' # TODO data['order_quantities_known'] = batch.order_quantities_known data['cases_shipped'] = row.cases_shipped data['units_shipped'] = row.units_shipped data['cases_received'] = row.cases_received data['units_received'] = row.units_received data['cases_damaged'] = row.cases_damaged data['units_damaged'] = row.units_damaged data['cases_expired'] = row.cases_expired data['units_expired'] = row.units_expired # TODO: surely the caller of API should determine this flag? # maybe alert user if they've already received some of this product alert_received = self.rattail_config.getbool('tailbone', 'receiving.alert_already_received', default=False) if alert_received: msg = '' if self.handler.get_units_confirmed(row): msg = "You have already received some of this product; last update was {}.".format( humanize.naturaltime(make_utc() - row.modified)) data['received_alert'] = msg return data def collection_get(self): return self._collection_get() def get(self): return self._get() def includeme(config): ReceivingBatchViews.defaults(config) ReceivingBatchRowViews.defaults(config)