# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
##
## Copyright (C) 2005-2012 Async Open Source <http://www.async.com.br>
## All rights reserved
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU Lesser General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program 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 Lesser General Public License for more details.
##
## You should have received a copy of the GNU Lesser General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., or visit: http://www.gnu.org/.
##
## Author(s): Stoq Team <stoq-devel@async.com.br>
##
"""This module contains classes centered around physical addresses.
There are two classes, :class:`Address` and :class:`CityLocation`.
CityLocation contains the city, state and country, Address contains
street, district, postal code and a reference to a |person|.
"""
# pylint: enable=E1101
from storm.expr import And
from storm.references import Reference
from storm.store import AutoReload
from zope.interface import implementer
from stoqlib.database.expr import StoqNormalizeString
from stoqlib.database.orm import ORMObject
from stoqlib.database.properties import UnicodeCol, IntCol, BoolCol, IdCol
from stoqlib.domain.base import Domain
from stoqlib.domain.interfaces import IDescribable
from stoqlib.l10n.l10n import get_l10n_field
from stoqlib.lib.formatters import format_address
from stoqlib.lib.parameters import sysparam
from stoqlib.lib.translation import stoqlib_gettext
_ = stoqlib_gettext
def _get_equal_clause(table, value):
# FIXME: Never versions of Psycopg2 treats str as bytes,
# We should do the same and enable:
# from __future__ import unicode_literals
# and start to convert all APIs to use unicode instead of str.
if isinstance(value, str):
value = unicode(value, 'utf-8')
return (StoqNormalizeString(table) ==
StoqNormalizeString(value))
# CityLocation inherits from ORMObject to avoid having te_id for a table
# that never is modified after initial import.
[docs]class CityLocation(ORMObject):
"""CityLocation is a class that contains the location of a city
and it's state/country. There are also codes for the city and states.
The country is expected to be one of the countries returned from
:func:`stoqlib.lib.countries.get_countries`.
See also:
`schema <http://doc.stoq.com.br/schema/tables/city_location.html>`__
.. note:: the city and state codes are currently Brazil specific
and refers to a unique identifier which is the same as
NFe (Nota Fiscal Eletronico) requires.
"""
__storm_table__ = 'city_location'
id = IntCol(primary=True, default=AutoReload)
#: the city
city = UnicodeCol(default=u"")
# FIXME: state should probably be renamed, as it's an administratal
# subdistrict fo a country.
#: the state
state = UnicodeCol(default=u"")
#: the country, iso-3166 localized using iso-codes
country = UnicodeCol(default=u"")
#: code of the city
city_code = IntCol(default=None)
#: code of the state
state_code = IntCol(default=None)
#
# Classmethods
#
@classmethod
[docs] def get_default(cls, store):
"""Get the default city location according to the database parameters.
The is usually the same city as main branch.
:returns: the default city location
"""
city = sysparam.get_string('CITY_SUGGESTED')
state = sysparam.get_string('STATE_SUGGESTED')
country = sysparam.get_string('COUNTRY_SUGGESTED')
return cls.get_or_create(store, city, state, country)
@classmethod
[docs] def get_or_create(cls, store, city, state, country):
"""
Get or create a city location. City locations are created lazily,
so this is used when registering new addresses.
:param store: a store
:param unicode city: a city
:param unicode state: a state
:param unicode country: a country
:returns: the |citylocation| or ``None``
"""
# FIXME: This should use find().one(). See bug 5146
location = list(store.find(cls,
And(_get_equal_clause(cls.city, city),
_get_equal_clause(cls.state, state),
_get_equal_clause(cls.country, country))))
if len(location) == 1:
return location[0]
elif len(location) > 1:
# Choose the best entry from city_location (the one we created)
for l in location:
if l.city_code:
return l
# Otherwise, return any object
return location[0]
return cls(city=city,
state=state,
country=country,
store=store)
@classmethod
[docs] def get_cities_by(cls, store, state=None, country=None):
"""Fetch a list of cities given a state and a country.
:param store: a store
:param state: state or ``None``
:param country: country or ``None``
:returns: a list of cities
:rtype: string
"""
clauses = []
if state:
clauses.append(_get_equal_clause(cls.state, state))
if country:
clauses.append(_get_equal_clause(cls.country, country))
if clauses:
results = store.find(cls, And(*clauses))
else:
results = store.find(cls)
return set(result.city for result in results)
@classmethod
def exists(cls, store, city, state, country):
# FIXME: This should use find().one(), but its possible to register
# duplicate city locations (see bug 5146)
return bool(store.find(cls, And(
_get_equal_clause(cls.city, city),
_get_equal_clause(cls.state, state),
_get_equal_clause(cls.country, country))).count())
#
# Public API
#
def is_valid_model(self):
city_l10n = get_l10n_field('city', self.country)
return bool(self.country and self.city and self.state and
city_l10n.validate(self.city,
state=self.state, country=self.country))
@implementer(IDescribable)
[docs]class Address(Domain):
"""An Address is a class that stores a physical street location
for a |person|.
A Person can have many addresses.
The city, state and country is found in |citylocation|.
See also:
`schema <http://doc.stoq.com.br/schema/tables/address.html>`__
"""
__storm_table__ = 'address'
#: street of the address, something like ``"Wall street"``
street = UnicodeCol(default=u'')
#: streetnumber, eg ``100``
streetnumber = IntCol(default=None)
#: district, eg ``"Manhattan"``
district = UnicodeCol(default=u'')
#: postal code, eg ``"12345-678"``
postal_code = UnicodeCol(default=u'')
#: complement, eg ``"apartment 35"``
complement = UnicodeCol(default=u'')
#: If this is the primary address for the |person|, this is set
#: when you register a person for the first time.
is_main_address = BoolCol(default=False)
person_id = IdCol()
#: the |person| who resides at this address
person = Reference(person_id, 'Person.id')
city_location_id = IntCol()
#: the |citylocation| this address is in
city_location = Reference(city_location_id, 'CityLocation.id')
#
# IDescribable
#
[docs] def get_description(self):
"""See `IDescribable.get_description()`"""
return self.get_address_string()
# Public API
[docs] def is_valid_model(self):
"""Verifies if this model is properly filled in,
that there's a street, district and valid |citylocation| set.
:returns: ``True`` if this address is filled in.
"""
# FIXME: This should probably take uiforms into account.
return (self.street and self.district and
self.city_location.is_valid_model())
[docs] def get_city(self):
"""Get the city for this address. It's fetched from
the |citylocation|.
:returns: the city
"""
return self.city_location.city
[docs] def get_country(self):
"""Get the country for this address. It's fetched from
the |citylocation|.
:returns: the country
"""
return self.city_location.country
[docs] def get_state(self):
"""Get the state for this address. It's fetched from
the |citylocation|.
:returns: the state
"""
return self.city_location.state
[docs] def get_postal_code_number(self):
"""Get the postal code without any non-numeric characters.
:returns: the postal code as a number
"""
if not self.postal_code:
return 0
return int(''.join([c for c in self.postal_code
if c in u'1234567890']))
[docs] def get_address_string(self):
"""Formats the address as a string
:returns: the formatted address
"""
return format_address(self)
[docs] def get_details_string(self):
""" Returns a string like ``postal_code - city - state``.
If city or state are missing, return only postal_code; and
if postal_code is missing, return ``city - state``, otherwise,
return an empty string
:returns: the detailed string
"""
details = []
if self.postal_code:
details.append(self.postal_code)
if self.city_location.city and self.city_location.state:
details.extend([self.city_location.city,
self.city_location.state])
details = u" - ".join(details)
return details