# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-20234 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/>.
#
################################################################################
"""
Data Models for People
"""
import datetime
import warnings
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.orderinglist import ordering_list
from .core import Base, uuid_column, Note
from .contact import PhoneNumber, EmailAddress, MailingAddress, ContactMixin
from rattail.db.util import normalize_full_name
# TODO: deprecate/remove this
def get_person_display_name(first_name, last_name):
return normalize_full_name(first_name, last_name)
# TODO: rename this?
def get_person_display_name_from_context(context):
first_name = context.current_parameters.get('first_name')
last_name = context.current_parameters.get('last_name')
return normalize_full_name(first_name, last_name)
[docs]
class Person(ContactMixin, Base):
"""
Represents a real, living and breathing person.
(Or, at least was previously living and breathing, in the case of the
deceased.)
"""
__tablename__ = 'person'
__versioned__ = {
'exclude': ['modified'],
}
uuid = uuid_column()
first_name = sa.Column(sa.String(length=50), nullable=True, doc="""
Person's "true" first name.
""")
preferred_first_name = sa.Column(sa.String(length=50), nullable=True, doc="""
Preferred first name for the person, if applicable. When present,
this may be used *instead of* the ``first_name`` value, when
displaying the person's full name etc.
""")
middle_name = sa.Column(sa.String(length=50), nullable=True, doc="""
Person's middle name (or initial), if applicable/known.
""")
last_name = sa.Column(sa.String(length=50), nullable=True, doc="""
Person's last name.
""")
# TODO: rename this to 'full_name' at some point...?
display_name = sa.Column(sa.String(length=100), nullable=True)
local_only = sa.Column(sa.Boolean(), nullable=True, doc="""
Flag indicating the person is somehow specific to the "local" app node
etc. and should not be synced elsewhere.
""")
invalid_address = sa.Column(sa.Boolean(), nullable=True, doc="""
Flag indicating the person's mailing address(es) on file are invalid.
""")
modified = sa.Column(sa.DateTime(), nullable=True, onupdate=datetime.datetime.utcnow)
def __str__(self):
return self.display_name or ""
@property
def user(self):
if self.users:
return self.users[0]
# TODO: deprecate / remove this
def add_email_address(self, address, type='Home', flush=False):
email = self.add_email(type=type, address=address, flush=flush)
return email
# TODO: deprecate / remove this
def add_phone_number(self, number, type='Home', flush=False):
phone = self.add_phone(type=type, number=number, flush=flush)
return phone
# TODO: deprecate / remove this
[docs]
def first_valid_email(self):
"""
Returns the first :class:`Email` which has not been marked invalid, or ``None``.
"""
for email in self.emails:
if not email.invalid:
return email
# TODO: deprecate / remove this
[docs]
def first_valid_email_address(self):
"""
Returns the first email address which has not been marked invalid, or ``None``.
"""
email = self.first_valid_email()
if email:
return email.address
[docs]
def only_customer(self, require=True):
"""
DEPRECATED
Convenience method to retrieve the one and only ``Customer`` record
which is associated with this person. An error will be raised if there
is not exactly one customer associated.
"""
warnings.warn("person.only_customer() is deprecated; please "
"use app.get_customer(person) instead",
DeprecationWarning, stacklevel=2)
# TODO: all 3 options below are indeterminate, since it's
# *possible* for a person to hold multiple accounts
# etc. but not sure how to fix in a generic way? maybe
# just everyone must override as needed
if self.customer_accounts:
return self.customer_accounts[0]
for shopper in self.customer_shoppers:
if shopper.shopper_number == 1:
return shopper.customer
# legacy fallback
if self.customers:
return self.customers[0]
if require:
raise ValueError(f"person {self.uuid} has no customer")
[docs]
class PersonPhoneNumber(PersonContactInfoMixin, PhoneNumber):
"""
Represents a phone (or fax) number associated with a person.
"""
__mapper_args__ = {'polymorphic_identity': 'Person'}
Person._contact_phone_model = PersonPhoneNumber
Person.phones = orm.relationship(
PersonPhoneNumber,
primaryjoin=PersonPhoneNumber.parent_uuid == Person.uuid,
foreign_keys=[PersonPhoneNumber.parent_uuid],
collection_class=ordering_list('preference', count_from=1),
order_by=PersonPhoneNumber.preference,
cascade='save-update, merge, delete, delete-orphan',
doc="""
Sequence of :class:`PersonPhoneNUmber` instances which belong to the
person.
""",
backref=orm.backref(
'person',
cascade_backrefs=False,
doc="""
Reference to the :class:`Person` instance to which the phone number
belongs.
"""),
)
Person.phone = orm.relationship(
PersonPhoneNumber,
primaryjoin=sa.and_(
PersonPhoneNumber.parent_uuid == Person.uuid,
PersonPhoneNumber.preference == 1,
),
foreign_keys=[PersonPhoneNumber.parent_uuid],
uselist=False,
viewonly=True)
[docs]
class PersonEmailAddress(PersonContactInfoMixin, EmailAddress):
"""
Represents an email address associated with a person.
"""
__mapper_args__ = {'polymorphic_identity': 'Person'}
Person._contact_email_model = PersonEmailAddress
Person.emails = orm.relationship(
PersonEmailAddress,
primaryjoin=PersonEmailAddress.parent_uuid == Person.uuid,
foreign_keys=[PersonEmailAddress.parent_uuid],
collection_class=ordering_list('preference', count_from=1),
order_by=PersonEmailAddress.preference,
cascade='save-update, merge, delete, delete-orphan',
doc="""
Sequence of :class:`PersonEmailAddress` instances which belong to the
person.
""",
backref=orm.backref(
'person',
cascade_backrefs=False,
doc="""
Reference to the :class:`Person` instance to which the email address
belongs.
"""),
)
Person.email = orm.relationship(
PersonEmailAddress,
primaryjoin=sa.and_(
PersonEmailAddress.parent_uuid == Person.uuid,
PersonEmailAddress.preference == 1,
),
foreign_keys=[PersonEmailAddress.parent_uuid],
uselist=False,
viewonly=True)
[docs]
class PersonMailingAddress(PersonContactInfoMixin, MailingAddress):
"""
Represents a physical / mailing address associated with a person.
"""
__mapper_args__ = {'polymorphic_identity': 'Person'}
Person._contact_address_model = PersonMailingAddress
Person.addresses = orm.relationship(
PersonMailingAddress,
backref='person',
primaryjoin=PersonMailingAddress.parent_uuid == Person.uuid,
foreign_keys=[PersonMailingAddress.parent_uuid],
collection_class=ordering_list('preference', count_from=1),
order_by=PersonMailingAddress.preference,
cascade='save-update, merge, delete, delete-orphan')
Person.address = orm.relationship(
PersonMailingAddress,
primaryjoin=sa.and_(
PersonMailingAddress.parent_uuid == Person.uuid,
PersonMailingAddress.preference == 1,
),
foreign_keys=[PersonMailingAddress.parent_uuid],
uselist=False,
viewonly=True)
[docs]
class PersonNote(Note):
"""
Represents a note attached to a person.
"""
__mapper_args__ = {'polymorphic_identity': 'Person'}
Person.notes = orm.relationship(
PersonNote,
primaryjoin=PersonNote.parent_uuid == Person.uuid,
foreign_keys=[PersonNote.parent_uuid],
order_by=PersonNote.created,
cascade='all, delete-orphan',
doc="""
Sequence of notes which belong to the person.
""",
backref=orm.backref(
'person',
cascade_backrefs=False,
doc="""
Reference to the person to which the note is attached.
"""))
[docs]
class MergePeopleRequest(Base):
"""
Represents a *request* to merge 2 people. If the request is no
longer wanted then it should be deleted. Otherwise it should be
updated to reflect who satisfies the request, by performing the
merge.
"""
__tablename__ = 'merge_request_people'
__table_args__ = (
sa.ForeignKeyConstraint(['requested_by_uuid'], ['user.uuid'],
name='merge_request_people_fk_requested_by'),
sa.ForeignKeyConstraint(['merged_by_uuid'], ['user.uuid'],
name='merge_request_people_fk_merged_by'),
)
uuid = uuid_column()
removing_uuid = sa.Column(sa.String(length=32), nullable=False, doc="""
UUID of the "unwanted" person, which is to be merged into the
"preserved" person.
""")
keeping_uuid = sa.Column(sa.String(length=32), nullable=False, doc="""
UUID of the person to be "preserved", into which the "unwanted"
person is to be merged.
""")
requested = sa.Column(sa.DateTime(), nullable=False, default=datetime.datetime.utcnow, doc="""
Date and time when the request was made.
""")
requested_by_uuid = sa.Column(sa.String(length=32), nullable=False)
requested_by = orm.relationship(
'User',
foreign_keys=[requested_by_uuid],
doc="""
Reference to the User who created the request.
""")
merged = sa.Column(sa.DateTime(), nullable=True, doc="""
Date and time when the merge was performed.
""")
merged_by_uuid = sa.Column(sa.String(length=32), nullable=True)
merged_by = orm.relationship(
'User',
foreign_keys=[merged_by_uuid],
doc="""
Reference to the User who performed the merge.
""")
def __str__(self):
return "Person Merge Request: {} -> {}".format(
self.removing_uuid, self.keeping_uuid)