Changeset - d720f0f0abd7
[Not reviewed]
0 2 0
Lance Edgar (lance) - 3 years ago 2021-11-03 20:19:23
lance@edbob.org
Add setting for "product price may be questionable" for custorders
2 files changed with 24 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 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)
 

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

	
 
        # always reset "add to customer" flags
 
        batch.clear_param('add_phone_number')
 
        batch.clear_param('add_email_address')
 

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

	
 
    def get_contact(self, batch):
 
        """
 
        Should return the contact record (i.e. Customer or Person) for
 
        the batch.
 
        """
 
        customer_required = self.new_order_requires_customer()
 

	
 
        if customer_required:
 
            return batch.customer
 
        else:
 
            return batch.person
 

	
 
    def get_contact_id(self, batch):
 
        """
 
        Should return contact ID for the batch, i.e. customer ID.
 
        """
 
        contact = self.get_contact(batch)
 
        if isinstance(contact, model.Customer):
 
            return contact.id
 

	
 
    def get_contact_display(self, batch):
 
        """
 
        Should return contact display text for the batch,
 
        i.e. customer name.
 
        """
 
        contact = self.get_contact(batch)
 
        if contact:
 
            return six.text_type(contact)
 

	
 
        pending = batch.pending_customer
 
        if pending:
 
            return six.text_type(pending)
 

	
 
    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.
 
        """
 
        phones = []
 
        contact = self.get_contact(batch)
 
        if contact:
 
            phones = contact.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.
 
        """
 
        emails = []
 
        contact = self.get_contact(batch)
 
        if contact:
 
            emails = contact.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 = []
 

	
 
        invalid = False
 
        contact = self.get_contact(batch)
 
        if contact:
 
            invalid = [email for email in contact.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
 

	
 
        # note that if batch already has a "pending" customer on file,
 
        # we will "restore" it as the contact info for the batch
 
        pending = batch.pending_customer
 
        if pending:
 
            batch.contact_name = pending.display_name
 
            batch.phone_number = pending.phone_number
 
            batch.email_address = pending.email_address
 
        else:
 
            batch.contact_name = None
 
            batch.phone_number = None
 
            batch.email_address = None
 

	
 
        # always reset "add to customer" flags
 
        batch.clear_param('add_phone_number')
 
        batch.clear_param('add_email_address')
 

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

	
 
    def validate_pending_customer_data(self, batch, user, data):
 
        pass
 

	
 
    def update_pending_customer(self, batch, user, data):
 
        model = self.model
 
        people = self.app.get_people_handler()
 

	
 
        # first validate all data
 
        self.validate_pending_customer_data(batch, user, data)
 

	
 
        # clear out any contact it may have
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
 

	
 
class rattail_custorders_product_price_may_be_questionable(Setting):
 
    """
 
    If set, then user may indicate that the price for a given product
 
    is "questionable" - which normally would cause a new step in the
 
    workflow, for someone to update and/or confirm the price.  If
 
    *not* set then user cannot mark any price as questionable.
 
    """
 
    group = "Customer Orders"
 
    namespace = 'rattail.custorders'
 
    name = 'product_price_may_be_questionable'
 
    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
 

	
 

	
0 comments (0 inline, 0 general)