Changeset - 331828e9ced1
[Not reviewed]
0 3 0
Lance Edgar (lance) - 3 years ago 2021-11-06 20:31:02
lance@edbob.org
Add support for finding past items, for new custorder

also misc. tweaks for new custorder feature
3 files changed with 46 insertions and 3 deletions:
0 comments (0 inline, 0 general)
rattail/app.py
Show inline comments
 
@@ -26,192 +26,196 @@ App Handler
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import os
 
# import re
 
import tempfile
 

	
 
from sqlalchemy import orm
 

	
 
from rattail.util import load_object, pretty_quantity, progress_loop
 
from rattail.files import temp_path
 
from rattail.mail import send_email
 

	
 

	
 
class AppHandler(object):
 
    """
 
    Base class and default implementation for top-level Rattail app handler.
 

	
 
    aka. "the handler to handle all handlers"
 

	
 
    aka. "one handler to bind them all"
 
    """
 
    default_autocompleters = {
 
        'brands': 'rattail.autocomplete.brands:BrandAutocompleter',
 
        'customers': 'rattail.autocomplete.customers:CustomerAutocompleter',
 
        'customers.neworder': 'rattail.autocomplete.customers:CustomerNewOrderAutocompleter',
 
        'customers.phone': 'rattail.autocomplete.customers:CustomerPhoneAutocompleter',
 
        'employees': 'rattail.autocomplete.employees:EmployeeAutocompleter',
 
        'departments': 'rattail.autocomplete.departments:DepartmentAutocompleter',
 
        'people': 'rattail.autocomplete.people:PersonAutocompleter',
 
        'people.employees': 'rattail.autocomplete.people:PersonEmployeeAutocompleter',
 
        'people.neworder': 'rattail.autocomplete.people:PersonNewOrderAutocompleter',
 
        'products': 'rattail.autocomplete.products:ProductAutocompleter',
 
        'products.all': 'rattail.autocomplete.products:ProductAllAutocompleter',
 
        'products.neworder': 'rattail.autocomplete.products:ProductNewOrderAutocompleter',
 
        'vendors': 'rattail.autocomplete.vendors:VendorAutocompleter',
 
    }
 

	
 
    def __init__(self, config):
 
        self.config = config
 

	
 
    def get_autocompleter(self, key, **kwargs):
 
        """
 
        Returns a new :class:`~rattail.autocomplete.base.Autocompleter`
 
        instance corresponding to the given key, e.g. ``'products'``.
 

	
 
        The app handler has some hard-coded defaults for the built-in
 
        autocompleters (see ``default_autocompleters`` in the source
 
        code).  You can override any of these, and/or add your own
 
        with custom keys, via config, e.g.:
 

	
 
        .. code-block:: ini
 

	
 
           [rattail]
 
           autocomplete.products = poser.autocomplete.products:ProductAutocompleter
 
           autocomplete.otherthings = poser.autocomplete.things:OtherThingAutocompleter
 

	
 
        With the above you can then fetch your custom autocompleter with::
 

	
 
           autocompleter = app.get_autocompleter('otherthings')
 

	
 
        In any case if it can locate the class, it will create an
 
        instance of it and return that.
 

	
 
        :params key: Unique key for the type of autocompleter you
 
           need.  Often is a simple string, e.g. ``'customers'`` but
 
           sometimes there may be a "modifier" with it to get an
 
           autocompleter with more specific behavior.
 

	
 
           For instance ``'customers.phone'`` would effectively give
 
           you a customer autocompleter but which searched by phone
 
           number instead of customer name.
 

	
 
           Note that each key is still a simple string though, and that
 
           must be "unique" in the sense that only one autocompleter
 
           can be configured for each key.
 

	
 
        :returns: An :class:`~rattail.autocomplete.base.Autocompleter`
 
           instance if found, otherwise ``None``.
 
        """
 
        spec = self.config.get('rattail', 'autocomplete.{}'.format(key))
 
        if not spec:
 
            spec = self.default_autocompleters.get(key)
 
        if spec:
 
            return load_object(spec)(self.config)
 

	
 
        raise NotImplementedError("cannot locate autocompleter for key: {}".format(key))
 

	
 
    def get_auth_handler(self, **kwargs):
 
        if not hasattr(self, 'auth_handler'):
 
            spec = self.config.get('rattail', 'auth.handler',
 
                                   default='rattail.auth:AuthHandler')
 
            factory = load_object(spec)
 
            self.auth_handler = factory(self.config, **kwargs)
 
        return self.auth_handler
 

	
 
    def get_batch_handler(self, key, **kwargs):
 
        from rattail.batch import get_batch_handler
 
        return get_batch_handler(self.config, key, **kwargs)
 

	
 
    def get_board_handler(self, **kwargs):
 
        if not hasattr(self, 'board_handler'):
 
            from rattail.board import get_board_handler
 
            self.board_handler = get_board_handler(self.config, **kwargs)
 
        return self.board_handler
 

	
 
    def get_clientele_handler(self, **kwargs):
 
        if not hasattr(self, 'clientele_handler'):
 
            from rattail.clientele import get_clientele_handler
 
            self.clientele_handler = get_clientele_handler(self.config, **kwargs)
 
        return self.clientele_handler
 

	
 
    def get_employment_handler(self, **kwargs):
 
        if not hasattr(self, 'employment_handler'):
 
            from rattail.employment import get_employment_handler
 
            self.employment_handler = get_employment_handler(self.config, **kwargs)
 
        return self.employment_handler
 

	
 
    def get_feature_handler(self, **kwargs):
 
        if not hasattr(self, 'feature_handler'):
 
            from rattail.features import FeatureHandler
 
            self.feature_handler = FeatureHandler(self.config, **kwargs)
 
        return self.feature_handler
 

	
 
    def get_email_handler(self, **kwargs):
 
        if not hasattr(self, 'email_handler'):
 
            from rattail.mail import get_email_handler
 
            self.email_handler = get_email_handler(self.config, **kwargs)
 
        return self.email_handler
 

	
 
    # TODO: is it helpful to expose this? or more confusing?
 
    get_mail_handler = get_email_handler
 

	
 
    def get_membership_handler(self, **kwargs):
 
        """
 
        Returns a reference to the configured Membership Handler.
 

	
 
        See also :doc:`rattail-manual:base/handlers/other/membership`.
 
        """
 
        if not hasattr(self, 'membership_handler'):
 
            spec = self.config.get('rattail', 'membership.handler',
 
                                   default='rattail.membership:MembershipHandler')
 
            factory = load_object(spec)
 
            self.membership_handler = factory(self.config, **kwargs)
 
        return self.membership_handler
 

	
 
    def get_people_handler(self, **kwargs):
 
        """
 
        Returns a reference to the configured People Handler.
 

	
 
        See also :doc:`rattail-manual:base/handlers/other/people`.
 
        """
 
        if not hasattr(self, 'people_handler'):
 
            spec = self.config.get('rattail', 'people.handler',
 
                                   default='rattail.people:PeopleHandler')
 
            factory = load_object(spec)
 
            self.people_handler = factory(self.config, **kwargs)
 
        return self.people_handler
 

	
 
    def get_products_handler(self, **kwargs):
 
        if not hasattr(self, 'products_handler'):
 
            from rattail.products import get_products_handler
 
            self.products_handler = get_products_handler(self.config, **kwargs)
 
        return self.products_handler
 

	
 
    def get_report_handler(self, **kwargs):
 
        if not hasattr(self, 'report_handler'):
 
            from rattail.reporting import get_report_handler
 
            self.report_handler = get_report_handler(self.config, **kwargs)
 
        return self.report_handler
 

	
 
    def progress_loop(self, *args, **kwargs):
 
        return progress_loop(*args, **kwargs)
 

	
 
    def get_session(self, obj):
 
        """
 
        Returns the SQLAlchemy session with which the given object is
 
        associated.  Simple convenience wrapper around
 
        ``sqlalchemy.orm.object_session()``.
 
        """
 
        return orm.object_session(obj)
 

	
 
    def make_session(self, **kwargs):
 
        """
 
        Creates and returns a new SQLAlchemy session for the Rattail DB.
 
        """
 
        from rattail.db import Session
 
        return Session(**kwargs)
 

	
 
    def cache_model(self, session, model, **kwargs):
 
        """
 
        Convenience method which invokes
 
        :func:`rattail.db.cache.cache_model()` with the given model
 
        and keyword arguments.
 
        """
 
        from rattail.db import cache
rattail/batch/custorder.py
Show inline comments
 
# -*- coding: utf-8; -*-
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2021 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 <http://www.gnu.org/licenses/>.
 
#
 
################################################################################
 
"""
 
Handler for "customer order" batches
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import, division
 

	
 
import re
 
import decimal
 

	
 
import six
 
import sqlalchemy as sa
 
from sqlalchemy import orm
 

	
 
from rattail.db import model
 
from rattail.batch import BatchHandler
 
from rattail.util import OrderedDict
 

	
 

	
 
class CustomerOrderBatchHandler(BatchHandler):
 
    """
 
    Handler for all "customer order" batches, regardless of "mode".  The
 
    handler must inspect the
 
    :attr:`~rattail.db.model.batch.custorder.CustomerOrderBatch.mode` attribute
 
    of each batch it deals with, in order to determine which logic to apply.
 

	
 
    .. attribute:: has_custom_product_autocomplete
 

	
 
       If true, this flag indicates that the handler provides custom
 
       autocomplete logic for use when selecting a product while
 
       creating a new order.
 
    """
 
    batch_model_class = model.CustomerOrderBatch
 
    has_custom_product_autocomplete = False
 
    nondigits_pattern = re.compile(r'\D')
 

	
 
    def init_batch(self, batch, progress=None, **kwargs):
 
        """
 
        Assign the "local" store to the batch, if applicable.
 
        """
 
        session = self.app.get_session(batch)
 
        batch.store = self.config.get_store(session)
 

	
 
    def new_order_requires_customer(self):
 
        """
 
        Returns a boolean indicating whether a *new* "customer order"
 
        in fact requires a proper customer account, or not.  Note that
 
        in all cases a new order requires a *person* to associate
 
        with, but technically the customer is optional, unless this
 
        returns true.
 
        """
 
        return self.config.getbool('rattail.custorders',
 
                                   'new_order_requires_customer',
 
                                   default=False)
 

	
 
    def allow_contact_info_choice(self):
 
        """
 
        Returns a boolean indicating whether the user is allowed at
 
        all, to choose from existing contact info options for the
 
        customer, vs. they just have to go with whatever the handler
 
        auto-provides.
 
        """
 
        return self.config.getbool('rattail.custorders',
 
                                   'new_orders.allow_contact_info_choice',
 
                                   default=True)
 

	
 
    def should_restrict_contact_info(self):
 
        """
 
        Returns a boolean indicating whether contact info should be
 
        "restricted" - i.e. user can only choose from existing contact
 
        info and cannot override by e.g. entering a new phone number.
 
        """
 
        return self.config.getbool('rattail.custorders',
 
                                   'new_orders.restrict_contact_info',
 
                                   default=False)
 

	
 
    def product_price_may_be_questionable(self):
 
        """
 
        Returns a boolean indicating whether "any" product's price may
 
        be questionable.  So this isn't saying that a price *is*
 
        questionable but rather that it *may* be, if the user
 
        indicates it.  (That checkbox is only shown for the user if
 
        this flag is true.)
 
        """
 
        return self.config.getbool('rattail.custorders',
 
                                   'product_price_may_be_questionable',
 
                                   default=False)
 

	
 
    def assign_contact(self, batch, customer=None, person=None, **kwargs):
 
        """
 
        Assign the customer and/or person "contact" for the order.
 
        """
 
        clientele = self.app.get_clientele_handler()
 
        customer_required = self.new_order_requires_customer()
 

	
 
        # nb. person is always required
 
        if customer and not person:
 
            person = clientele.get_person(customer)
 
        if not person:
 
            raise ValueError("Must specify a person")
 

	
 
        # customer may or may not be optional
 
        if person and not customer:
 
            customer = clientele.get_customer(person)
 
        if customer_required and not customer:
 
            raise ValueError("Must specify a customer account")
 

	
 
        # assign contact
 
        batch.customer = customer
 
        batch.person = person
 

	
 
        # cache contact name
 
        batch.contact_name = self.get_contact_display(batch)
 
@@ -317,228 +318,258 @@ class CustomerOrderBatchHandler(BatchHandler):
 
            pending.display_name = data['display_name']
 
        else:
 
            pending.display_name = people.normalize_full_name(pending.first_name,
 
                                                              pending.last_name)
 
        if 'phone_number' in data:
 
            pending.phone_number = self.app.format_phone_number(data['phone_number'])
 
        if 'email_address' in data:
 
            pending.email_address = data['email_address']
 

	
 
        # also update the batch w/ contact info
 
        batch.contact_name = pending.display_name
 
        batch.phone_number = pending.phone_number
 
        batch.email_address = pending.email_address
 

	
 
    def get_case_size_for_product(self, product):
 
        if product.case_size:
 
            return product.case_size
 

	
 
        cost = product.cost
 
        if cost:
 
            return cost.case_size
 

	
 
    def get_case_price_for_row(self, row):
 
        """
 
        Calculate and return the per-case price for the given row.
 

	
 
        NB. we do not store case price, only unit price.  maybe that
 
        should change some day..
 
        """
 
        if row.unit_price is not None:
 
            case_price = row.unit_price * (row.case_quantity or 1)
 
            return case_price.quantize(decimal.Decimal('0.01'))
 

	
 
    # TODO: this method should maybe not exist?  and caller just
 
    # invokes the handler directly instead?
 
    def customer_autocomplete(self, session, term, **kwargs):
 
        """
 
        Override the Customer autocomplete, to search by phone number
 
        as well as name.
 
        """
 
        autocompleter = self.app.get_autocompleter('customers.neworder')
 
        return autocompleter.autocomplete(session, term, **kwargs)
 

	
 
    # TODO: this method should maybe not exist?  and caller just
 
    # invokes the handler directly instead?
 
    def person_autocomplete(self, session, term, **kwargs):
 
        """
 
        Override the Person autocomplete, to search by phone number as
 
        well as name.
 
        """
 
        autocompleter = self.app.get_autocompleter('people.neworder')
 
        return autocompleter.autocomplete(session, term, **kwargs)
 

	
 
    def get_customer_info(self, batch, **kwargs):
 
        """
 
        Return a data dict containing misc. info pertaining to the
 
        customer/person for the order batch.
 
        """
 
        info = {
 
            'customer_uuid': None,
 
            'person_uuid': None,
 
            'phone_number': None,
 
            'email_address': None,
 
        }
 

	
 
        if batch.customer:
 
            info['customer_uuid'] = batch.customer.uuid
 
            phone = batch.customer.first_phone()
 
            if phone:
 
                info['phone_number'] = phone.number
 
            email = batch.customer.first_email()
 
            if email:
 
                info['email_address'] = email.address
 

	
 
        if batch.person:
 
            info['person_uuid'] = batch.person.uuid
 
            if not info['phone_number']:
 
                phone = batch.person.first_phone()
 
                if phone:
 
                    info['phone_number'] = phone.number
 
                email = batch.person.first_email()
 
                if email:
 
                    info['email_address'] = email.address
 

	
 
        return info
 

	
 
    def custom_product_autocomplete(self, session, term, **kwargs):
 
        """
 
        For the given term, this should return a (possibly empty) list
 
        of products which "match" the term.  Each element in the list
 
        should be a dict with "label" and "value" keys.
 
        """
 
        raise NotImplementedError("Please define the "
 
                                  "{}.custom_product_autocomplete() "
 
                                  "method.".format(__class__.__name__))
 

	
 
    def get_past_orders(self, batch, **kwargs):
 
        """
 
        Retrieve a list of past orders for the batch contact.
 
        """
 
        session = self.app.get_session(batch)
 
        model = self.model
 
        orders = session.query(model.CustomerOrder)
 

	
 
        contact = self.get_contact(batch)
 
        if isinstance(contact, model.Customer):
 
            orders = orders.filter(model.CustomerOrder.customer == contact)
 
        else:
 
            orders = orders.filter(model.CustomerOrder.person == contact)
 

	
 
        orders = orders.order_by(model.CustomerOrder.created.desc())
 
        return orders.all()
 

	
 
    def get_past_products(self, batch, **kwargs):
 
        """
 
        Should return a (possibly empty) list of products which have
 
        been ordered in the past by the customer who is associated
 
        with the given batch.
 
        """
 
        # TODO: should crawl the rattail order history here
 
        return []
 
        session = self.app.get_session(batch)
 
        model = self.model
 
        products = OrderedDict()
 

	
 
        # track down all order items for batch contact
 
        orders = self.get_past_orders(batch)
 
        for order in orders:
 
            for item in order.items:
 
                product = item.product
 
                if product:
 
                    # we only want the first match for each product
 
                    products.setdefault(product.uuid, product)
 

	
 
        return list(products.values())
 

	
 
    def get_product_info(self, batch, product, **kwargs):
 
        """
 
        Return a data dict containing misc. info pertaining to the
 
        given product, for the order batch.
 
        """
 
        products = self.app.get_products_handler()
 
        vendor = product.cost.vendor if product.cost else None
 
        info = {
 
            'uuid': product.uuid,
 
            'upc': six.text_type(product.upc),
 
            'upc_pretty': product.upc.pretty(),
 
            'brand_name': product.brand.name if product.brand else None,
 
            'description': product.description,
 
            'size': product.size,
 
            'full_description': product.full_description,
 
            'case_quantity': self.app.render_quantity(self.get_case_size_for_product(product)),
 
            'unit_price_display': products.render_price(product.regular_price),
 
            'department_name': product.department.name if product.department else None,
 
            'vendor_name': vendor.name if vendor else None,
 
            'url': products.get_url(product),
 
            'image_url': products.get_image_url(product),
 
            'uom_choices': self.uom_choices_for_product(product),
 
        }
 

	
 
        case_price = None
 
        if product.regular_price and product.regular_price is not None:
 
            case_size = self.get_case_size_for_product(product)
 
            case_price = case_size * product.regular_price.price
 
            case_price = (case_size or 1) * product.regular_price.price
 
            case_price = case_price.quantize(decimal.Decimal('0.01'))
 
        info['case_price'] = six.text_type(case_price) if case_price is not None else None
 
        info['case_price_display'] = self.app.render_currency(case_price)
 

	
 
        key = self.config.product_key()
 
        if key == 'upc':
 
            info['key'] = info['upc_pretty']
 
        else:
 
            info['key'] = getattr(product, key, info['upc_pretty'])
 

	
 
        return info
 

	
 
    def uom_choices_for_product(self, product):
 
        """
 
        Return a list of UOM choices for the given product.
 
        """
 
        choices = []
 

	
 
        # Each
 
        if not product or not product.weighed:
 
            unit_name = self.enum.UNIT_OF_MEASURE[self.enum.UNIT_OF_MEASURE_EACH]
 
            choices.append({'key': self.enum.UNIT_OF_MEASURE_EACH,
 
                            'value': unit_name})
 

	
 
        # Pound
 
        if not product or product.weighed:
 
            unit_name = self.enum.UNIT_OF_MEASURE[self.enum.UNIT_OF_MEASURE_POUND]
 
            choices.append({
 
                'key': self.enum.UNIT_OF_MEASURE_POUND,
 
                'value': unit_name,
 
            })
 

	
 
        # Case
 
        case_text = None
 
        case_size = self.get_case_size_for_product(product)
 
        if case_size is None:
 
            case_text = "{} (&times; ?? {})".format(
 
                self.enum.UNIT_OF_MEASURE[self.enum.UNIT_OF_MEASURE_CASE],
 
                unit_name)
 
        elif case_size > 1:
 
            case_text = "{} (&times; {} {})".format(
 
                self.enum.UNIT_OF_MEASURE[self.enum.UNIT_OF_MEASURE_CASE],
 
                self.app.render_quantity(case_size),
 
                unit_name)
 
        if case_text:
 
            choices.append({'key': self.enum.UNIT_OF_MEASURE_CASE,
 
                            'value': case_text})
 

	
 
        return choices
 

	
 
    def why_not_add_product(self, product, batch):
 
        """
 
        This method can inspect the given product, and batch, to
 
        determine if the product may be added to the batch as a new
 
        row.  Useful to e.g. prevent one customer from ordering too
 
        many things, etc.
 

	
 
        :returns: If there is a reason not to add the product, should
 
           return that reason as a string; otherwise ``None``.
 
        """
 

	
 
    def add_product(self, batch, product, order_quantity, order_uom,
 
                    **kwargs):
 
        """
 
        Add a new row to the batch, for the given product and order
 
        quantity.
 
        """
 
        row = self.make_row()
 
        row.item_entry = product.uuid
 
        row.product = product
 
        row.order_quantity = order_quantity
 
        row.order_uom = order_uom
 
        if 'price_needs_confirmation' in kwargs:
 
            row.price_needs_confirmation = kwargs['price_needs_confirmation']
 
        self.add_row(batch, row)
 
        return row
 

	
 
    def refresh_row(self, row):
 
        if not row.product:
 
            if row.item_entry:
 
                session = orm.object_session(row)
 
                # TODO: should do more than just query for uuid here
 
                product = session.query(model.Product).get(row.item_entry)
 
                if product:
 
                    row.product = product
 
            if not row.product:
 
                row.status_code = row.STATUS_PRODUCT_NOT_FOUND
 
                return
 

	
 
        product = row.product
 
        row.product_upc = product.upc
 
        row.product_brand = six.text_type(product.brand or "")
 
        row.product_description = product.description
 
        row.product_size = product.size
 
        row.product_weighed = product.weighed
 
        row.case_quantity = self.get_case_size_for_product(product)
rattail/products.py
Show inline comments
 
@@ -87,192 +87,200 @@ class ProductsHandler(GenericHandler):
 
           Either way of course the list might be empty.
 
        """
 
        from rattail.db.api.products import (get_product_by_upc,
 
                                             get_product_by_item_id,
 
                                             get_product_by_scancode,
 
                                             get_product_by_code,
 
                                             get_product_by_vendor_code)
 

	
 
        model = self.model
 
        only = kwargs.get('only')
 
        if isinstance(only, six.string_types):
 
            only = [only]
 
        vendor = kwargs.get('vendor')
 
        include_keys = kwargs.get('include_keys', False)
 
        products = []
 

	
 
        # don't bother if we're given empty value
 
        if not value:
 
            return products
 

	
 
        # TODO: most of the function calls below are only good for "at
 
        # most one" result.  in some cases there may be more than one
 
        # match in fact, which will raise an error.  need to refactor
 
        # somehow to account for that..for now just pass `only` param
 
        # to avoid the problematic keys for your situation.
 

	
 
        # maybe look for 'uuid' match
 
        if not only or 'uuid' in only:
 
            product = session.query(model.Product).get(value)
 
            if product:
 
                products.append(('uuid', product))
 

	
 
        # maybe look for 'upc' match
 
        if not only or 'upc' in only:
 

	
 
            # if value is a GPC we kind of only have one thing to try
 
            if isinstance(value, GPC):
 
                product = get_product_by_upc(session, value)
 
                if product:
 
                    products.append(('upc', product))
 

	
 
            else: # not GPC, so must convert
 

	
 
                if value.isdigit():
 

	
 
                    # we first assume the value *does* include check digit
 
                    provided = GPC(value, calc_check_digit=False)
 
                    product = get_product_by_upc(session, provided)
 
                    if product:
 
                        products.append(('upc', product))
 

	
 
                    # but we can also calculate a check digit and try that
 
                    checked = GPC(value, calc_check_digit='upc')
 
                    product = get_product_by_upc(session, checked)
 
                    if product:
 
                        products.append(('upc', product))
 

	
 
                    # one last trick is to expand UPC-E to UPC-A and then reattempt
 
                    # the lookup, *with* check digit (since it would be known)
 
                    if len(value) in (6, 8):
 
                        checked = GPC(upce_to_upca(value), calc_check_digit='upc')
 
                        product = get_product_by_upc(session, checked)
 
                        if product:
 
                            products.append(('upc', product))
 

	
 
        # maybe look for 'item_id' match
 
        if not only or 'item_id' in only:
 
            product = get_product_by_item_id(session, value)
 
            if product:
 
                products.append(('item_id', product))
 

	
 
        # maybe look for 'scancode' match
 
        if not only or 'scancode' in only:
 
            product = get_product_by_scancode(session, value)
 
            if product:
 
                products.append(('scancode', product))
 

	
 
        # maybe look for 'altcode' match
 
        if not only or 'altcode' in only:
 
            product = get_product_by_code(session, value)
 
            if product:
 
                products.append(('altcode', product))
 

	
 
        # maybe look for 'sku' match
 
        if not only or 'sku' in only:
 
            product = get_product_by_vendor_code(session, value,
 
                                                 vendor=vendor)
 
            if product:
 
                products.append(('sku', product))
 

	
 
        # maybe strip keys out of the result
 
        if not include_keys:
 
            products = [tup[1] for tup in products]
 

	
 
        return products
 

	
 
    def get_url(self, product, **kwargs):
 
        """
 
        Return the Tailbone "view" URL for the given product.
 
        """
 
        base_url = self.config.base_url()
 
        if base_url:
 
            return '{}/products/{}'.format(base_url, product.uuid)
 

	
 
    def get_image_url(self, product=None, upc=None, **kwargs):
 
        """
 
        Return the preferred image URL for the given UPC or product.
 
        """
 
        base_url = self.config.base_url()
 

	
 
        # we prefer the "image on file" if available
 
        if base_url and product and product.image:
 
            return '{}/products/{}/image'.format(base_url, product.uuid)
 

	
 
        # and if this product is a pack item, then we prefer the unit
 
        # item image as fallback, if available
 
        if base_url and product and product.is_pack_item():
 
            unit = product.unit
 
            if unit and unit.image:
 
                return '{}/products/{}/image'.format(base_url, unit.uuid)
 

	
 
        # fallback to the POD image, if available and so configured
 
        if self.config.getbool('tailbone', 'products.show_pod_image',
 
                               default=False):
 
            if product and not upc:
 
                upc = product.upc
 
            if upc:
 
                return self.get_pod_image_url(upc)
 

	
 
        if base_url:
 
            return '{}/tailbone/img/product.png'.format(base_url)
 

	
 
    def get_pod_image_url(self, upc, **kwargs):
 
        """
 
        Return the POD image URL for the given UPC.
 
        """
 
        if upc:
 
            return pod.get_image_url(self.config, upc)
 

	
 
    def render_price(self, price, html=False, **kwargs):
 
        """
 
        Render the given ``price`` object as text.
 

	
 
        :returns: String containing the rendered price, or ``None`` if
 
           nothing was applicable.
 
        """
 
        if price.price is not None and price.pack_price is not None:
 
            if price.multiple > 1:
 
                return "{} / {}  ({} / {})".format(
 
                    self.app.render_currency(price.price),
 
                    price.multiple,
 
                    self.app.render_currency(price.pack_price),
 
                    price.pack_multiple)
 
            return "{}  ({} / {})".format(
 
                self.app.render_currency(price.price),
 
                self.app.render_currency(price.pack_price),
 
                price.pack_multiple)
 
        if price.price is not None:
 
            if price.multiple is not None and price.multiple > 1:
 
                return "{} / {}".format(
 
                    self.app.render_currency(price.price),
 
                    price.multiple)
 
            return self.app.render_currency(price.price)
 
        if price.pack_price is not None:
 
            return "{} / {}".format(
 
                self.app.render_currency(price.pack_price),
 
                price.pack_multiple)
 

	
 
    def get_uom_sil_codes(self, session, uppercase=False, **kwargs):
 
        """
 
        This should return a dict, keys of which are UOM abbreviation strings,
 
        and values of which are corresponding SIL code strings.
 

	
 
        :param session: Reference to current Rattail DB session.
 
        :param uppercase: Set to ``True`` to cause all UOM abbreviations to be
 
           upper-cased when adding to the map.
 
        :returns: Dictionary containing all known UOM / SIL code mappings.
 
        """
 
        model = self.model
 

	
 
        def normalize(uom):
 
            if uom.sil_code:
 
                return uom.sil_code
 

	
 
        def make_key(uom, normal):
 
            key = uom.abbreviation
 
            if uppercase:
 
                key = key.upper()
 
            return key
 

	
 
        return self.cache_model(session, model.UnitOfMeasure,
 
                                normalizer=normalize,
 
                                key=make_key)
 

	
 
    def get_uom_sil_code(self, session, uom, uppercase=False, **kwargs):
 
        """
 
        This should return a SIL code which corresponds to the given UOM
 
        abbreviation string.  Useful when you just need one out of the blue,
 
        but if you need multiple codes looked up then you're probably better
 
        off using :meth:`get_uom_sil_codes()` for efficiency.
0 comments (0 inline, 0 general)