Changeset - 785f98547e48
[Not reviewed]
0 4 0
Lance Edgar (lance) - 3 years ago 2021-09-27 08:08:54
lance@edbob.org
Make custorder batch handler responsible for (un)assigning contact

it also will update the contact info, i.e. phone/email
4 files changed with 220 insertions and 4 deletions:
0 comments (0 inline, 0 general)
rattail/batch/custorder.py
Show inline comments
 
@@ -51,87 +51,228 @@ class CustomerOrderBatchHandler(BatchHandler):
 
    """
 
    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 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()
 
        session.refresh(batch)
 

	
 
    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 customer name.
 
        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 customer name
 
            # 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:
rattail/clientele.py
Show inline comments
 
@@ -63,23 +63,67 @@ class ClienteleHandler(GenericHandler):
 
        """
 
        return customer.only_person(require=False)
 

	
 
    def make_customer(self, person, **kwargs):
 
        """
 
        Create and return a new customer record.
 
        """
 
        customer = self.model.Customer()
 
        customer.name = person.display_name
 
        customer.people.append(person)
 
        return customer
 

	
 
    def get_first_phone(self, customer, **kwargs):
 
        """
 
        Return the first available phone record found, either for the
 
        customer, or its first person.
 
        """
 
        phone = customer.first_phone()
 
        if phone:
 
            return phone
 

	
 
        person = self.get_person(customer)
 
        if person:
 
            return person.first_phone()
 

	
 
    def get_first_phone_number(self, customer, **kwargs):
 
        """
 
        Return the first available phone number found, either for the
 
        customer, or its first person.
 
        """
 
        phone = self.first_phone(customer)
 
        if phone:
 
            return phone.number
 

	
 
    def get_first_email(self, customer, **kwargs):
 
        """
 
        Return the first available email record found, either for the
 
        customer, or its first person.
 
        """
 
        email = customer.first_email()
 
        if email:
 
            return email
 

	
 
        person = self.get_person(customer)
 
        if person:
 
            return person.first_email()
 

	
 
    def get_first_email_address(self, customer, **kwargs):
 
        """
 
        Return the first available email address found, either for the
 
        customer, or its first person.
 
        """
 
        email = self.first_email(customer)
 
        if email:
 
            return email.address
 

	
 

	
 
def get_clientele_handler(config, **kwargs):
 
    """
 
    Create and return the configured :class:`ClienteleHandler` instance.
 
    """
 
    spec = config.get('rattail', 'clientele.handler')
 
    if spec:
 
        factory = load_object(spec)
 
    else:
 
        factory = ClienteleHandler
 
    return factory(config, **kwargs)
rattail/db/model/contact.py
Show inline comments
 
# -*- coding: utf-8; -*-
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2020 Lance Edgar
 
#  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.
 
@@ -146,24 +146,32 @@ class ContactMixin(object):
 
    # TODO: subclass must set these
 
    _contact_email_model = None
 
    _contact_phone_model = None
 
    _contact_address_model = None
 

	
 
    def first_email(self, **kwargs):
 
        """
 
        Return the first available email record for the contact.
 
        """
 
        if self.emails:
 
            return self.emails[0]
 

	
 
    def first_email_address(self, **kwargs):
 
        """
 
        Return the first available email address for the contact.
 
        """
 
        email = self.first_email()
 
        if email:
 
            return email.address
 

	
 
    def make_email(self, **kwargs):
 
        """
 
        Make a new "email" record for the contact.
 
        """
 
        email = self._contact_email_model(**kwargs)
 
        return email
 

	
 
    def add_email(self, **kwargs):
 
        """
 
        Add a new "email" record to the contact.
 
        """
 
        flush = kwargs.pop('flush', True)
 
@@ -203,24 +211,32 @@ class ContactMixin(object):
 
        self.emails.remove(email)
 
        if flush:
 
            session = orm.object_session(self)
 
            session.flush()
 

	
 
    def first_phone(self, **kwargs):
 
        """
 
        Return the first available phone record for the contact.
 
        """
 
        if self.phones:
 
            return self.phones[0]
 

	
 
    def first_phone_number(self, **kwargs):
 
        """
 
        Return the first available phone number for the contact.
 
        """
 
        phone = self.first_phone()
 
        if phone:
 
            return phone.number
 

	
 
    def make_phone(self, **kwargs):
 
        """
 
        Make a new "phone" record for the contact.
 
        """
 
        # set some safe defaults in case session is flushed early
 
        kwargs.setdefault('number', '')
 
        phone = self._contact_phone_model(**kwargs)
 
        return phone
 

	
 
    def add_phone(self, **kwargs):
 
        """
 
        Add a new "phone" record to the contact.
rattail/settings.py
Show inline comments
 
@@ -104,24 +104,39 @@ class rattail_appdir(Setting):
 
    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
 

	
 

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

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

	
0 comments (0 inline, 0 general)