Changeset - 63864c95f177
[Not reviewed]
0 2 0
Lance Edgar (lance) - 3 years ago 2021-10-06 11:43:01
lance@edbob.org
Add setting to allow contact info choice for new custorder
2 files changed with 22 insertions and 0 deletions:
0 comments (0 inline, 0 general)
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 six
 
import sqlalchemy as sa
 
from sqlalchemy import orm
 

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

	
 

	
 
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 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
 

	
 
        # update phone/email per new contact
 
        batch.phone_number = None
 
        batch.email_address = None
 
        if customer_required:
 
            batch.phone_number = clientele.get_first_phone_number(customer)
 
            batch.email_address = clientele.get_first_email_address(customer)
 
        else:
 
            batch.phone_number = person.first_phone_number()
 
            batch.email_address = person.first_email_address()
 

	
 
        session = self.app.get_session(batch)
 
        session.flush()
 

	
 
    def get_contact_display(self, batch):
 
        """
 
        Should return contact display text for the batch,
 
        i.e. customer name.
 
        """
 
        customer_required = self.new_order_requires_customer()
 

	
 
        if customer_required:
 
            return six.text_type(batch.customer)
 
        else:
 
            return six.text_type(batch.person)
 

	
 
    def get_contact_phones(self, batch):
 
        """
 
        Retrieve all phone records on file for the batch contact, to
 
        be presented as options for user to choose from when making a
 
        new order.
 
        """
 
        customer_required = self.new_order_requires_customer()
 

	
 
        phones = []
 
        if customer_required:
 
            if batch.customer:
 
                phones = batch.customer.phones
 
        else:
 
            if batch.person:
 
                phones = batch.person.phones
 

	
 
        return [self.normalize_phone(phone)
 
                for phone in phones]
 

	
 
    def normalize_phone(self, phone):
 
        """
 
        Normalize the given phone record to simple data dict, for
 
        passing around via JSON etc.
 
        """
 
        return {
 
            'uuid': phone.uuid,
 
            'type': phone.type,
 
            'number': phone.number,
 
            'preference': phone.preference,
 
            'preferred': phone.preference == 1,
 
        }
 

	
 
    def get_contact_emails(self, batch):
 
        """
 
        Retrieve all email records on file for the batch contact, to
 
        be presented as options for user to choose from when making a
 
        new order.
 

	
 
        Note that the default logic will exclude invalid email addresses.
 
        """
 
        customer_required = self.new_order_requires_customer()
 

	
 
        emails = []
 
        if customer_required:
 
            if batch.customer:
 
                emails = batch.customer.emails
 
        else:
 
            if batch.person:
 
                emails = batch.person.emails
 

	
 
        # exclude invalid
 
        emails = [email for email in emails
 
                  if not email.invalid]
 

	
 
        return [self.normalize_email(email)
 
                for email in emails]
 

	
 
    def normalize_email(self, email):
 
        """
 
        Normalize the given email record to simple data dict, for
 
        passing around via JSON etc.
 
        """
 
        return {
 
            'uuid': email.uuid,
 
            'type': email.type,
 
            'address': email.address,
 
            'invalid': email.invalid,
 
            'preference': email.preference,
 
            'preferred': email.preference == 1,
 
        }
 

	
 
    def get_contact_notes(self, batch):
 
        """
 
        Get extra "contact notes" which should be made visible to the
 
        user who is entering the new order.
 
        """
 
        notes = []
 
        customer_required = self.new_order_requires_customer()
 

	
 
        invalid = False
 
        if customer_required:
 
            if batch.customer:
 
                invalid = [email for email in batch.customer.emails
 
                           if email.invalid]
 
        else:
 
            if batch.person:
 
                invalid = [email for email in batch.person.emails
 
                           if email.invalid]
 
        if invalid:
 
            notes.append("Customer has one or more invalid email addresses on file.")
 

	
 
        return notes
 

	
 
    def unassign_contact(self, batch, **kwargs):
 
        """
 
        Unassign the customer and/or person "contact" for the order.
 
        """
 
        batch.customer = None
 
        batch.person = None
 
        batch.phone_number = None
 
        batch.email_address = None
 

	
 
        session = self.app.get_session(batch)
 
        session.flush()
 
        session.refresh(batch)
 

	
 
    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_phone_search_term(self, term):
 
        """
 
        Try to figure out if the given search term represents a whole
 
        or partial phone number, and if so return just the digits.
 
        """
 
        digits = self.nondigits_pattern.sub('', term)
 
        if digits and len(digits) >= 4:
 
            return digits
 

	
 
    def customer_autocomplete(self, session, term, **kwargs):
 
        """
 
        Override the Customer autocomplete, to search by phone number
 
        as well as name.
 
        """
 
        model = self.model
 

	
 
        # define the base query
 
        query = session.query(model.Customer)\
 
                       .options(orm.joinedload(model.Customer.phones))
 

	
 
        # does search term look like a phone number?
 
        phone_term = self.get_phone_search_term(term)
 
        if phone_term:
 

	
 
            # yep, so just search for the phone number
 
            query = query.join(model.CustomerPhoneNumber,
 
                               model.CustomerPhoneNumber.parent_uuid == model.Customer.uuid)
 
            query = query.filter(sa.func.regexp_replace(model.CustomerPhoneNumber.number,
 
                                                        r'\D', '', 'g')\
 
                                 .like('%{}%'.format(phone_term)))
 

	
 
        else: # term does not look like a phone number
 

	
 
            # so just search by name
 
            criteria = [model.Customer.name.ilike('%{}%'.format(word))
 
                        for word in term.split()]
 
            query = query.filter(sa.and_(*criteria))
 

	
 
        # oh, and sort by something useful
 
        query = query.order_by(model.Customer.name)
 

	
 
        # generate result list from query
 
        results = []
 
        for customer in query:
 
            phone = customer.first_phone()
 
            if phone:
 
                label = "{} {}".format(customer.name, phone.number)
 
            else:
 
                label = customer.name
 
            results.append({'value': customer.uuid,
 
                            'label': label,
 
                            'display': customer.name})
 

	
 
        return results
 

	
 
    def person_autocomplete(self, session, term, **kwargs):
 
        """
 
        Override the Person autocomplete, to search by phone number as
 
        well as name.
 
        """
 
        model = self.model
 

	
 
        # define the base query
 
        query = session.query(model.Person)\
 
                       .options(orm.joinedload(model.Person.phones))
 

	
 
        # does search term look like a phone number?
 
        phone_term = self.get_phone_search_term(term)
 
        if phone_term:
 

	
 
            # yep, so just search for the phone number
 
            query = query.join(model.PersonPhoneNumber,
 
                               model.PersonPhoneNumber.parent_uuid == model.Person.uuid)
 
            query = query.filter(sa.func.regexp_replace(model.PersonPhoneNumber.number,
 
                                                        r'\D', '', 'g')\
 
                                 .like('%{}%'.format(phone_term)))
 

	
 
        else: # term does not look like a phone number
 

	
 
            # so just search by name
 
            criteria = [model.Person.display_name.ilike('%{}%'.format(word))
 
                        for word in term.split()]
 
            query = query.filter(sa.and_(*criteria))
 

	
 
        # oh, and sort by something useful
 
        query = query.order_by(model.Person.display_name)
 

	
 
        # generate result list from query
 
        results = []
 
        for person in query:
 
            phone = person.first_phone()
 
            if phone:
 
                label = "{} {}".format(person.display_name, phone.number)
 
            else:
 
                label = person.display_name
 
            results.append({'value': person.uuid,
 
                            'label': label,
 
                            'display': person.display_name})
 

	
 
        return results
 

	
 
    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 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)
 

	
 
        department = product.department
 
        row.department_number = department.number if department else None
 
        row.department_name = department.name if department else None
 

	
 
        cost = product.cost
 
        row.product_unit_cost = cost.unit_cost if cost else None
 

	
 
        regprice = product.regular_price
 
        row.unit_price = regprice.price if regprice else None
 

	
 
        # we need to know if total price is updated
 
        old_total = row.total_price
 

	
 
        # maybe update total price
 
        if row.unit_price is None:
 
            row.total_price = None
 
        elif not row.unit_price:
 
            row.total_price = 0
 
        else:
 
            row.total_price = row.unit_price * row.order_quantity
 
            if row.order_uom == self.enum.UNIT_OF_MEASURE_CASE:
 
                row.total_price *= (row.case_quantity or 1)
 

	
 
        # update total price for batch too, if it changed
 
        if row.total_price != old_total:
 
            batch = row.batch
 
            batch.total_price = ((batch.total_price or 0)
 
                                 + (row.total_price or 0)
 
                                 - (old_total or 0))
 

	
 
        row.status_code = row.STATUS_OK
 

	
 
    def remove_row(self, row):
 
        batch = row.batch
 

	
 
        if not row.removed:
 
            row.removed = True
 

	
 
            if row.total_price:
 
                batch.total_price = (batch.total_price or 0) - row.total_price
 

	
 
        self.refresh_batch_status(batch)
 

	
 
    def execute(self, batch, user=None, progress=None, **kwargs):
 
        """
 
        Default behavior here will simply create a new (proper) Customer Order
 
        based on the batch contents.  Override as needed.
 
        """
 
        batch_fields = [
 
            'store',
 
            'id',
 
            'customer',
 
            'person',
 
            'phone_number',
 
            'email_address',
 
            'total_price',
 
        ]
 

	
 
        order = model.CustomerOrder()
 
        order.created_by = user
 
        order.status_code = self.enum.CUSTORDER_STATUS_ORDERED
 
        for field in batch_fields:
 
            setattr(order, field, getattr(batch, field))
 

	
 
        row_fields = [
 
            'product',
 
            'product_upc',
 
            'product_brand',
 
            'product_description',
 
            'product_size',
 
            'product_weighed',
 
            'department_number',
 
            'department_name',
 
            'case_quantity',
 
            'order_quantity',
 
            'order_uom',
 
            'product_unit_cost',
 
            'unit_price',
 
            'discount_percent',
 
            'total_price',
 
            'paid_amount',
 
            'payment_transaction_number',
 
        ]
 

	
 
        def convert(row, i):
 
            item = model.CustomerOrderItem()
 
            item.sequence = i
 
            item.status_code = self.enum.CUSTORDER_ITEM_STATUS_INITIATED
 
            for field in row_fields:
 
                setattr(item, field, getattr(row, field))
 
            order.items.append(item)
 

	
 
            # attach event
 
            item.events.append(model.CustomerOrderItemEvent(
 
                type_code=self.enum.CUSTORDER_ITEM_EVENT_INITIATED,
 
                user=user))
 

	
 
        self.progress_loop(convert, batch.active_rows(), progress,
 
                           message="Converting batch rows to order items")
 

	
 
        session = orm.object_session(batch)
 
        session.add(order)
 
        session.flush()
 

	
 
        return order
rattail/settings.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/>.
 
#
 
################################################################################
 
"""
 
Common setting definitions
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 

	
 
class Setting(object):
 
    """
 
    Base class for all setting definitions.
 
    """
 
    group = "(General)"
 
    namespace = None
 
    name = None
 
    data_type = str
 
    choices = None
 
    required = False
 

	
 

	
 
##############################
 
# (General)
 
##############################
 

	
 
class rattail_app_title(Setting):
 
    """
 
    Official display title for the app.
 
    """
 
    namespace = 'rattail'
 
    name = 'app_title'
 

	
 

	
 
class rattail_node_title(Setting):
 
    """
 
    Official display title for the app node.
 
    """
 
    namespace = 'rattail'
 
    name = 'node_title'
 

	
 

	
 
class rattail_production(Setting):
 
    """
 
    If set, the app is considered to be running in "production" mode, whereas
 
    if disabled, the app is considered to be running in development / testing /
 
    staging mode.
 
    """
 
    namespace = 'rattail'
 
    name = 'production'
 
    data_type = bool
 

	
 

	
 
class tailbone_background_color(Setting):
 
    """
 
    Background color for this app node.  If unset, default color is white.
 
    """
 
    namespace = 'tailbone'
 
    name = 'background_color'
 

	
 

	
 
class rattail_single_store(Setting):
 
    """
 
    If set, the app should assume there is only one Store record, and that all
 
    purchases etc. will pertain to it.
 
    """
 
    namespace = 'rattail'
 
    name = 'single_store'
 
    data_type = bool
 

	
 

	
 
class rattail_demo(Setting):
 
    """
 
    If set, the app is considered to be running in "demo" mode.
 
    """
 
    namespace = 'rattail'
 
    name = 'demo'
 
    data_type = bool
 

	
 

	
 
class rattail_appdir(Setting):
 
    """
 
    Path to the "app" dir for the running instance.
 
    """
 
    namespace = 'rattail'
 
    name = 'appdir'
 

	
 

	
 
class rattail_workdir(Setting):
 
    """
 
    Path to the "work" dir for the running instance.
 
    """
 
    namespace = 'rattail'
 
    name = 'workdir'
 

	
 

	
 
##############################
 
# Customer Orders
 
##############################
 

	
 
class rattail_custorders_new_order_requires_customer(Setting):
 
    """
 
    If set, then all new orders require a proper customer account.  If
 
    *not* set then just a "person" will suffice.
 
    """
 
    group = "Customer Orders"
 
    namespace = 'rattail.custorders'
 
    name = 'new_order_requires_customer'
 
    data_type = bool
 

	
 
class rattail_custorders_new_orders_allow_contact_info_choice(Setting):
 
    """
 
    If set, then user can choose from contact info options, when
 
    creating new order.  If *not* set then they cannot choose, and
 
    must use whatever the batch handler provides.
 
    """
 
    group = "Customer Orders"
 
    namespace = 'rattail.custorders'
 
    name = 'new_orders.allow_contact_info_choice'
 
    data_type = bool
 

	
 
class rattail_custorders_new_orders_restrict_contact_info(Setting):
 
    """
 
    If set, then user can only choose from existing contact info options,
 
    for the customer/order.  If *not* set, then user is allowed to enter
 
    new/different contact info.
 
    """
 
    group = "Customer Orders"
 
    namespace = 'rattail.custorders'
 
    name = 'new_orders.restrict_contact_info'
 
    data_type = bool
 

	
 

	
 
##############################
 
# DataSync
 
##############################
 

	
 
class rattail_datasync_url(Setting):
 
    """
 
    URL for datasync change queue.
 
    """
 
    group = "DataSync"
 
    namespace = 'rattail.datasync'
 
    name = 'url'
 

	
 

	
 
class tailbone_datasync_restart(Setting):
 
    """
 
    Command used when restarting the datasync daemon.
 
    """
 
    group = "DataSync"
 
    namespace = 'tailbone'
 
    name = 'datasync.restart'
 

	
 

	
 
##############################
 
# Email
 
##############################
 

	
 
class rattail_mail_record_attempts(Setting):
 
    """
 
    If enabled, this flag will cause Email Attempts to be recorded in the
 
    database, for "most" attempts to send email.
 
    """
 
    group = "Email"
 
    namespace = 'rattail.mail'
 
    name = 'record_attempts'
 
    data_type = bool
 

	
 

	
 
##############################
 
# FileMon
 
##############################
 

	
 
class tailbone_filemon_restart(Setting):
 
    """
 
    Command used when restarting the filemon daemon.
 
    """
 
    group = "FileMon"
 
    namespace = 'tailbone'
 
    name = 'filemon.restart'
 

	
 

	
 
##############################
 
# Inventory
 
##############################
 

	
 
class tailbone_inventory_force_unit_item(Setting):
 
    """
 
    Defines which of the possible "product key" fields should be effectively
 
    treated as the product key.
 
    """
 
    group = "Inventory"
 
    namespace = 'tailbone'
 
    name = 'inventory.force_unit_item'
 
    data_type = bool
 

	
 

	
 
##############################
 
# Products
 
##############################
 

	
 
class rattail_product_key(Setting):
 
    """
 
    Defines which of the possible "product key" fields should be effectively
 
    treated as the product key.
 
    """
 
    group = "Products"
 
    namespace = 'rattail'
 
    name = 'product.key'
 
    choices = [
 
        'upc',
 
        'item_id',
 
        'scancode',
 
    ]
 

	
 

	
 
class rattail_product_key_title(Setting):
 
    """
 
    Defines the official "title" (display name) for the product key field.
 
    """
 
    group = "Products"
 
    namespace = 'rattail'
 
    name = 'product.key_title'
 

	
 

	
 
class rattail_products_mobile_quick_lookup(Setting):
 
    """
 
    If set, the mobile Products page will only allow "quick lookup" access to
 
    product records.  If NOT set, then the typical record listing is shown.
 
    """
 
    group = "Products"
 
    namespace = 'rattail'
 
    name = 'products.mobile.quick_lookup'
 
    data_type = bool
 

	
 

	
 
class tailbone_products_show_pod_image(Setting):
 
    """
 
    If a product has an image within the database, it will be shown when
 
    viewing the product details.  If this flag is set, and the product has no
 
    image, then the "POD" image will be shown, if available.  If not set, the
 
    POD image will not be used as a fallback.
 
    """
 
    group = "Products"
 
    namespace = 'tailbone'
 
    name = 'products.show_pod_image'
 
    data_type = bool
 

	
 

	
 
##############################
 
# Purchasing / Receiving
 
##############################
 

	
 
class rattail_batch_purchase_allow_cases(Setting):
 
    """
 
    Determines whether or not "cases" is a valid UOM for ordering, receiving etc.
 
    """
 
    group = "Purchasing / Receiving"
 
    namespace = 'rattail.batch'
 
    name = 'purchase.allow_cases'
 
    data_type = bool
 

	
 

	
 
class rattail_batch_purchase_allow_expired_credits(Setting):
 
    """
 
    Determines whether or not "expired" is a valid type for purchase credits.
 
    """
 
    group = "Purchasing / Receiving"
 
    namespace = 'rattail.batch'
 
    name = 'purchase.allow_expired_credits'
 
    data_type = bool
 

	
 

	
 
class rattail_batch_purchase_allow_receiving_from_scratch(Setting):
 
    """
 
    Determines whether or not receiving "from scratch" is allowed.  In this
 
    mode, the batch starts out empty and receiver must add product to it over
 
    time.
 
    """
 
    group = "Purchasing / Receiving"
 
    namespace = 'rattail.batch'
 
    name = 'purchase.allow_receiving_from_scratch'
 
    data_type = bool
 

	
 

	
 
class rattail_batch_purchase_allow_receiving_from_invoice(Setting):
 
    """
 
    Determines whether or not receiving "from invoice" is allowed.  In this
 
    mode, the user must first upload an invoice file they wish to receive
 
    against.
 
    """
 
    group = "Purchasing / Receiving"
 
    namespace = 'rattail.batch'
 
    name = 'purchase.allow_receiving_from_invoice'
 
    data_type = bool
 

	
 

	
 
class rattail_batch_purchase_allow_receiving_from_purchase_order(Setting):
 
    """
 
    Determines whether or not receiving "from PO" is allowed.  In this mode,
 
    the user must first select the purchase order (PO) they wish to receive
 
    against.  The batch is initially populated with order quantities from the
 
    PO, and user then updates (or adds) rows over time.
 
    """
 
    group = "Purchasing / Receiving"
 
    namespace = 'rattail.batch'
 
    name = 'purchase.allow_receiving_from_purchase_order'
 
    data_type = bool
 

	
 

	
 
class rattail_batch_purchase_allow_truck_dump_receiving(Setting):
 
    """
 
    Determines whether or not "truck dump" receiving is allowed.  This is a
 
    rather complicated feature, where one "parent" truck dump batch is created
 
    for the receiver, plus several "child" batches, one for each invoice
 
    involved.
 
    """
 
    group = "Purchasing / Receiving"
 
    namespace = 'rattail.batch'
 
    name = 'purchase.allow_truck_dump_receiving'
 
    data_type = bool
 

	
 

	
 
class rattail_batch_purchase_mobile_images(Setting):
 
    """
 
    If set, product images will be displayed when viewing a purchasing batch row.
 
    """
 
    group = "Purchasing / Receiving"
 
    namespace = 'rattail.batch'
 
    name = 'purchase.mobile_images'
 
    data_type = bool
 

	
 

	
 
class rattail_batch_purchase_mobile_quick_receive(Setting):
 
    """
 
    If set, a "quick receive" button will be available for mobile receiving.
 
    """
 
    group = "Purchasing / Receiving"
 
    namespace = 'rattail.batch'
 
    name = 'purchase.mobile_quick_receive'
 
    data_type = bool
 

	
 

	
 
class rattail_batch_purchase_mobile_quick_receive_all(Setting):
 
    """
 
    If set, the mobile "quick receive" button will receive "all" (remaining
 
    quantity) for the item, instead of "one".
 
    """
 
    group = "Purchasing / Receiving"
 
    namespace = 'rattail.batch'
 
    name = 'purchase.mobile_quick_receive_all'
 
    data_type = bool
 

	
 

	
 
##############################
 
# Reporting
 
##############################
 

	
 
class tailbone_reporting_choosing_uses_form(Setting):
 
    """
 
    When generating a new report, if this flag is set then you will choose the
 
    report from a dropdown.  If the flag is not set then you will see all
 
    reports listed on the page and you'll click the link for one.
 
    """
 
    group = "Reporting"
 
    namespace = 'tailbone'
 
    name = 'reporting.choosing_uses_form'
 
    data_type = bool
 

	
 

	
 
##############################
 
# Vendors
 
##############################
 

	
 
class rattail_vendor_use_autocomplete(Setting):
 
    """
 
    If set, `vendor` fields will use the autocomplete widget; otherwise such
 
    fields will use a drop-down (select) widget.
 
    """
 
    group = "Vendors"
 
    namespace = 'rattail'
 
    name = 'vendor.use_autocomplete'
 
    data_type = bool
0 comments (0 inline, 0 general)