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

	
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):
 
    """
0 comments (0 inline, 0 general)