# -*- 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>
##
"""Person domain classes

The Person domain classes in Stoqlib are special since the :obj:`Person`
class is small and additional functionality is provided through
facets.

There are currently the following person facets available:

  * |branch| - a physical location within a company
  * |client| - when buying something from a branch
  * |company| - a company, tax entitity
  * |employee| - works for a branch
  * |individual| - physical person
  * |loginuser| - can login and use the system
  * :obj:`SalesPerson` - can sell to clients
  * |supplier| - provides product and services to a branch
  * |transporter| - transports deliveries to/from a branch

To create a new person, just issue the following::

    >>> from stoqlib.database.runtime import new_store
    >>> store = new_store()

    >>> person = Person(name=u"A new person", store=store)

Then to add a client, you can will do:

    >>> client = Client(person=person, store=store)

"""

import datetime
import hashlib

from kiwi.currency import currency
from storm.expr import And, Join, LeftJoin, Like, Or, Update
from storm.info import ClassAlias
from storm.references import Reference, ReferenceSet
from storm.store import EmptyResultSet
from zope.interface import implements

from stoqlib.database.expr import Age, Case, Date, DateTrunc, Interval
from stoqlib.database.properties import (BoolCol, DateTimeCol,
                                  IntCol, PercentCol,
                                  PriceCol,
                                  UnicodeCol)
from stoqlib.database.viewable import Viewable
from stoqlib.database.runtime import get_current_station
from stoqlib.domain.address import Address
from stoqlib.domain.base import Domain
from stoqlib.domain.event import Event
from stoqlib.domain.interfaces import IDescribable, IActive
from stoqlib.domain.payment.payment import Payment
from stoqlib.domain.sellable import ClientCategoryPrice
from stoqlib.domain.profile import UserProfile
from stoqlib.enums import LatePaymentPolicy
from stoqlib.exceptions import DatabaseInconsistency, SellError
from stoqlib.lib.formatters import raw_phone_number, format_phone_number
from stoqlib.lib.parameters import sysparam
from stoqlib.lib.translation import stoqlib_gettext

_ = stoqlib_gettext


#
# Base Domain Classes
#


class EmployeeRole(Domain):
    """Base class to store the |employee| roles."""

    implements(IDescribable)

    __storm_table__ = 'employee_role'

    name = UnicodeCol()

    #
    # IDescribable
    #

    def get_description(self):
        return self.name

    #
    # Public API
    #

    def has_other_role(self, name):
        """Check if there is another role with the same name

        :param name: name of the role to check
        :returns: ``True`` if it exists, otherwise ``False``
        """
        return self.check_unique_value_exists(
            EmployeeRole.name, name, case_sensitive=False)


# WorkPermitData, MilitaryData, and VoterData are Brazil-specific information.
class WorkPermitData(Domain):
    """Work permit data for an |employee|.

    .. note:: This is Brazil-specific information.
    """

    __storm_table__ = 'work_permit_data'

    number = UnicodeCol(default=None)
    series_number = UnicodeCol(default=None)
    #: number of PIS ("Programa de Integracao Social")
    pis_number = UnicodeCol(default=None)
    #: bank PIS ("Programa de Integracao Social")
    pis_bank = UnicodeCol(default=None)
    #: registry date of PIS ("Programa de Integracao Social")
    pis_registry_date = DateTimeCol(default=None)


class MilitaryData(Domain):
    """ Military data for an |employee|.

    .. note:: This is Brazil-specific information.

    """

    __storm_table__ = 'military_data'

    number = UnicodeCol(default=None)
    series_number = UnicodeCol(default=None)
    category = UnicodeCol(default=None)


class VoterData(Domain):
    """Voter data for an |employee|.

    .. note:: This is Brazil-specific information.
    """

    __storm_table__ = 'voter_data'

    number = UnicodeCol(default=None)
    section = UnicodeCol(default=None)
    zone = UnicodeCol(default=None)


class ContactInfo(Domain):
    """Class to store the person's contact information.
    This can be used to store:

    * phone numbers (land lines and mobile)
    * email addresses
    * web sites (corporate, home, Facebook, Google Plus)
    * IM contact information
    * contact of other people inside an organization"""

    implements(IDescribable)

    __storm_table__ = 'contact_info'

    #: describes what the contact information is, e.g. Home Phone Number
    description = UnicodeCol(default=u'')

    #: the contact information itself, e.g. 1234-5678, user@example.com, ...
    contact_info = UnicodeCol(default=u'')

    person_id = IntCol()

    #: the |person|
    person = Reference(person_id, 'Person.id')

    #
    # IDescribable
    #

    def get_description(self):
        return self.description


class CreditCheckHistory(Domain):
    """Client credit check history

    This stores credit information about a |client|.

    From time to time, a store may contact some 'credit protection agency' that
    will inform the status of a certain client, for instance, if the client has
    active debt with other companies.
    """

    __storm_table__ = 'credit_check_history'

    #: if a client has debt
    STATUS_INCLUDED = 0

    #: if a client does not have debt
    STATUS_NOT_INCLUDED = 1

    statuses = {STATUS_INCLUDED: _(u'Included'),
                STATUS_NOT_INCLUDED: _(u'Not included')}

    #: when this check was created
    creation_date = DateTimeCol(default_factory=datetime.datetime.now)

    #: when the check was made
    check_date = DateTimeCol()

    #: an unique identifier created by the agency
    identifier = UnicodeCol()

    #: the client status given the options above
    status = IntCol()

    #: notes about the credit check history created by the user
    notes = UnicodeCol()

    client_id = IntCol()

    #: the |client|
    client = Reference(client_id, 'Client.id')

    user_id = IntCol()

    #: the `user` that created this entry
    user = Reference(user_id, 'LoginUser.id')


class Calls(Domain):
    """Person's calls information.

    Calls are information associated to a |person| (|client|, |supplier|,
    |employee|, etc) that can be financial problems registries,
    collection letters information, some problems with a product
    delivered, etc.
    """

    implements(IDescribable)

    __storm_table__ = 'calls'

    date = DateTimeCol()
    description = UnicodeCol()
    message = UnicodeCol()

    person_id = IntCol()

    #: the |person|
    person = Reference(person_id, 'Person.id')

    attendant_id = IntCol()

    attendant = Reference(attendant_id, 'LoginUser.id')

    #
    # IDescribable
    #

    def get_description(self):
        return self.description


def _validate_number(person, attr, number):
    if number is None:
        number = u''
    return raw_phone_number(number)


# A Person can actually be thought of as a "Contactable", to use
# the same terminology as Storable/Sellable.
class Person(Domain):
    """A Person, an entity that can be contacted (via phone, email).
    It usually has an |address|.
    """

    __storm_table__ = 'person'

    # FIXME: These two are internal to person template and should be
    # moved there.
    (ROLE_INDIVIDUAL,
     ROLE_COMPANY) = range(2)

    #: name of the person, depending on the facets, it can either
    #: be something like "John Doe" or "Microsoft Corporation"
    name = UnicodeCol()

    #: phone number for this person
    phone_number = UnicodeCol(default=u'', validator=_validate_number)

    #: cell/mobile number for this person
    mobile_number = UnicodeCol(default=u'', validator=_validate_number)

    #: fax number for this person
    fax_number = UnicodeCol(default=u'', validator=_validate_number)

    #: email address
    email = UnicodeCol(default=u'')

    #: notes about the person
    notes = UnicodeCol(default=u'')

    #: all `contact information <ContactInfo>` related to this person
    contact_infos = ReferenceSet('id', 'ContactInfo.person_id')

    #: list of |addresses|
    addresses = ReferenceSet('id', 'Address.person_id')

    #: all `calls <Calls>` made to this person
    calls = ReferenceSet('id', 'Calls.person_id')

    #: the |branch| facet for this person
    branch = Reference('id', 'Branch.person_id', on_remote=True)

    #: the |client| facet for this person
    client = Reference('id', 'Client.person_id', on_remote=True)

    #: the |company| facet for this person
    company = Reference('id', 'Company.person_id', on_remote=True)

    #: |employee| facet for this person
    employee = Reference('id', 'Employee.person_id', on_remote=True)

    #: |individual| for this person
    individual = Reference('id', 'Individual.person_id', on_remote=True)

    #: |loginuser| facet for this person
    login_user = Reference('id', 'LoginUser.person_id', on_remote=True)

    #: the :obj:`sales person <SalesPerson>` facet for this person
    salesperson = Reference('id', 'SalesPerson.person_id', on_remote=True)

    #: the |supplier| facet for this person
    supplier = Reference('id', 'Supplier.person_id', on_remote=True)

    #: the |transporter| facet for this person
    transporter = Reference('id', 'Transporter.person_id', on_remote=True)

    @property
    def address(self):
        """The |address| for this person
        """
        return self.get_main_address()

    #
    # Classmethods
    #

    @classmethod
    def get_by_phone_number(cls, store, phone_number):
        """Returns a list of |person| given a specific phone_number.
        This looks at both phone_number and mobile_number.

        :param phone_number:
        :returns: a list of |person|
        """
        if not phone_number:
            return EmptyResultSet()

        phone_number = u'%%%s%%' % raw_phone_number(phone_number)
        query = Or(Like(cls.phone_number, phone_number),
                   Like(cls.mobile_number, phone_number))
        return store.find(cls, query)

    #
    # Acessors
    #

    def get_main_address(self):
        """The primary |address| for this person. It is normally
        set when you register the client for the first time.
        """
        return self.store.find(Address,
                                     person_id=self.id,
                                     is_main_address=True).one()

    def get_total_addresses(self):
        """The total number of |addresses| for this person.

        :returns: the number of |addresses|
        """
        return self.store.find(Address, person_id=self.id).count()

    def get_address_string(self):
        """The primary |address| for this person formatted as a string.

        :returns: the |address|
        """
        address = self.get_main_address()
        if not address:
            return u''
        return address.get_address_string()

    def get_phone_number_number(self):
        """Returns the phone number without any non-numeric characters

        :returns: the phone number as a number
        """
        if not self.phone_number:
            return 0
        return int(''.join([c for c in self.phone_number
                                  if c in u'1234567890']))

    def get_fax_number_number(self):
        """Returns the fax number without any non-numeric characters

        :returns: the fax number as a number
        """
        if not self.fax_number:
            return 0
        return int(''.join([c for c in self.fax_number
                                  if c in u'1234567890']))

    def get_formatted_phone_number(self):
        """
        :returns: a dash-separated phone number or an empty string
        """
        if self.phone_number:
            return format_phone_number(self.phone_number)
        return u""

    def get_formatted_fax_number(self):
        """
        :returns: a dash-separated fax number or an empty string
        """
        if self.fax_number:
            return format_phone_number(self.fax_number)
        return u""

    #
    # Public API
    #

    def has_individual_or_company_facets(self):
        return self.individual or self.company


class Individual(Domain):
    """Being or characteristic of a single person, concerning one
    person exclusively

    """

    implements(IActive, IDescribable)

    __storm_table__ = 'individual'

    (STATUS_SINGLE,
     STATUS_MARRIED,
     STATUS_DIVORCED,
     STATUS_WIDOWED,
     STATUS_SEPARATED,
     STATUS_COHABITATION) = range(6)

    marital_statuses = {STATUS_SINGLE: _(u"Single"),
                        STATUS_MARRIED: _(u"Married"),
                        STATUS_DIVORCED: _(u"Divorced"),
                        STATUS_WIDOWED: _(u"Widowed"),
                        STATUS_SEPARATED: _(u'Separated'),
                        STATUS_COHABITATION: _(u'Cohabitation')}

    (GENDER_MALE,
     GENDER_FEMALE) = range(2)

    genders = {GENDER_MALE: _(u'Male'),
               GENDER_FEMALE: _(u'Female')}

    person_id = IntCol()

    #: the |person|
    person = Reference(person_id, 'Person.id')

    # FIXME: rename to "document"
    #: the national document used to identify this person.
    cpf = UnicodeCol(default=u'')

    #: A Brazilian government register which identify an individual
    rg_number = UnicodeCol(default=u'')

    #: when this individual was born
    birth_date = DateTimeCol(default=None)

    #: current job
    occupation = UnicodeCol(default=u'')

    #: martial status, single, married, widow etc
    marital_status = IntCol(default=STATUS_SINGLE)

    #: Name of this individuals father
    father_name = UnicodeCol(default=u'')

    #: Name of this individuals mother
    mother_name = UnicodeCol(default=u'')

    #: When the rg number was issued
    rg_expedition_date = DateTimeCol(default=None)

    #: Where the rg number was issued
    rg_expedition_local = UnicodeCol(default=u'')

    #: male or female
    gender = IntCol(default=None)

    #: the name of the spouse individual's partner in marriage
    spouse_name = UnicodeCol(default=u'')

    birth_location_id = IntCol(default=None)

    #: the |location| where individual was born
    birth_location = Reference(birth_location_id, 'CityLocation.id')

    is_active = BoolCol(default=True)

    #
    # IActive
    #

    def inactivate(self):
        assert self.is_active, (u'This individual is already inactive')
        self.is_active = False

    def activate(self):
        assert not self.is_active, (u'This individual is already active')
        self.is_active = True

    def get_status_string(self):
        if self.is_active:
            return _(u'Active')
        return _(u'Inactive')

    #
    # IDescribable
    #

    def get_description(self):
        return self.person.name

    #
    # Public API
    #

    def get_marital_statuses(self):
        return [(self.marital_statuses[i], i)
                for i in self.marital_statuses.keys()]

    def get_cpf_number(self):
        """Returns the cpf number without any non-numeric characters

        :returns: the cpf number as a number
        """
        if not self.cpf:
            return 0
        return int(''.join([c for c in self.cpf if c in '1234567890']))

    def check_cpf_exists(self, cpf):
        """Returns ``True`` if we already have a Individual with the given CPF
        in the database.
        """
        return self.check_unique_value_exists(Individual.cpf, cpf)

    @classmethod
    def get_birthday_query(cls, start, end=None):
        """Get a database query suitable to use in a SearchColumn.search_func
             callback. This can either be searching for a birthday in a date or
             an interval of dates.

           :param start: start date
           :param end: for intervals, an end date, use `None` for single days
           :returns: the database query
        """
        start_year = DateTrunc(u'year', Date(start))
        age_in_year = Age(cls.birth_date, DateTrunc(u'year', cls.birth_date))
        next_birthday = (
            start_year + age_in_year +
            Case(condition=age_in_year < Age(Date(start), start_year),
                 result=Interval(u"1 year"),
                 else_=Interval(u"0 year"))
        )

        if end is None:
            return next_birthday == Date(start)
        else:
            return And(next_birthday >= Date(start),
                       next_birthday <= Date(end))


class Company(Domain):
    """An institution created to conduct business
    """

    implements(IActive, IDescribable)

    __storm_table__ = 'company'

    person_id = IntCol()

    #: the |person|
    person = Reference(person_id, 'Person.id')

    # FIXME: rename to document
    #: a number identifing the company
    cnpj = UnicodeCol(default=u'')

    #: Doing business as (dba) name for this company, a secondary, non-legal
    #: name of the company.
    fancy_name = UnicodeCol(default=u'')

    #: Brazilian register number associated with a certain state
    state_registry = UnicodeCol(default=u'')

    #: Brazilian register number associated with a certain city
    city_registry = UnicodeCol(default=u'')

    is_active = BoolCol(default=True)

    #
    # IActive
    #

    def inactivate(self):
        assert self.is_active, (u'This company is already inactive')
        self.is_active = False

    def activate(self):
        assert not self.is_active, (u'This company is already active')
        self.is_active = True

    def get_status_string(self):
        if self.is_active:
            return _(u'Active')
        return _(u'Inactive')

    #
    # IDescribable
    #

    def get_description(self):
        return self.person.name

    #
    # Public API
    #

    def get_cnpj_number(self):
        """Returns the cnpj number without any non-numeric characters

        :returns: the cnpj number as a number
        """
        if not self.cnpj:
            return 0

        # FIXME: We should return cnpj as strings, since it can begin with 0
        num = u''.join([c for c in self.cnpj if c in u'1234567890'])
        if num:
            return int(num)
        return 0

    def get_state_registry_number(self):
        """Returns the state registry number without any non-numeric characters

        :returns: the state registry number as a number or zero if there is
          no state registry.
        """
        if not self.state_registry:
            return 0

        numbers = u''.join([c for c in self.state_registry
                                    if c in u'1234567890'])
        return int(numbers or 0)

    def check_cnpj_exists(self, cnpj):
        """Returns ``True`` if we already have a Company with the given CNPJ
        in the database.
        """
        return self.check_unique_value_exists(Company.cnpj, cnpj)


class ClientCategory(Domain):
    """I am a client category.
    """

    implements(IDescribable)

    __storm_table__ = 'client_category'

    #: name of the category
    name = UnicodeCol()

    #: max discount for clients of this category
    max_discount = PercentCol(default=0)

    #
    # IDescribable
    #

    def get_description(self):
        return self.name

    #
    # Public API
    #

    def can_remove(self):
        """ Check if the client category is used in some product."""
        return self.store.find(ClientCategoryPrice, category=self).is_empty()

    def remove(self):
        """Remove this client category from the database."""
        self.store.remove(self)


class Client(Domain):
    """An individual or a company who pays for goods or services

    """

    implements(IActive, IDescribable)

    __storm_table__ = 'client'

    (STATUS_SOLVENT,
     STATUS_INDEBTED,
     STATUS_INSOLVENT,
     STATUS_INACTIVE) = range(4)

    statuses = {STATUS_SOLVENT: _(u'Solvent'),
                STATUS_INDEBTED: _(u'Indebted'),
                STATUS_INSOLVENT: _(u'Insolvent'),
                STATUS_INACTIVE: _(u'Inactive')}

    person_id = IntCol()

    #: the |person|
    person = Reference(person_id, 'Person.id')

    #: ok, indebted, insolvent, inactive
    status = IntCol(default=STATUS_SOLVENT)

    #: How many days is this client indebted
    days_late = IntCol(default=0)

    #: How much the user can spend on store credit
    credit_limit = PriceCol(default=0)

    category_id = IntCol(default=None)

    #: the :obj:`client category <ClientCategory>` for this client
    category = Reference(category_id, 'ClientCategory.id')

    #: client salary
    _salary = PriceCol(u'salary', default=0)

    #
    # IActive
    #

    def get_status_string(self):
        if not self.status in self.statuses:
            raise DatabaseInconsistency('Invalid status for client, '
                                        'got %d' % self.status)
        return self.statuses[self.status]

    def inactivate(self):
        if self.status == Client.STATUS_INACTIVE:
            raise AssertionError('This client is already inactive')
        self.status = self.STATUS_INACTIVE

    def activate(self):
        if self.status == Client.STATUS_SOLVENT:
            raise AssertionError('This client is already active')
        self.status = self.STATUS_SOLVENT

    def _get_is_active(self):
        return self.status == self.STATUS_SOLVENT

    def _set_is_active(self, value):
        if value:
            self.activate()
        else:
            self.inactivate()
    is_active = property(_get_is_active, _set_is_active)

    #
    # IDescribable
    #

    def get_description(self):
        return self.person.name

    #
    # Public API
    #

    def get_name(self):
        """Name of the client
        """
        return self.person.name

    @classmethod
    def get_active_clients(cls, store):
        """Return a list of active clients.
        An active client is a person who are authorized to make new sales
        """
        return store.find(cls, cls.status == cls.STATUS_SOLVENT)

    @classmethod
    def update_credit_limit(cls, percent, store):
        """Updates clients credit limit acordingly to the new percent informed.

        This perecentage is aplied to the client salary to calculate
        the credit limit.

        Only clients with an informed salary will have the credit limit
        updated.

        :param percent: The percentage value that will be used to calculate
          the new credit limit.
        """
        if percent == 0:
            return

        vals = {Client.credit_limit: Client._salary * percent / 100}
        clause = Client._salary > 0
        # XXX This will update the table, but storm wont reload the data. Maybe
        # we should invalidate all clients in cache
        store.execute(Update(vals, clause, Client))

    def get_client_sales(self):
        """Returns a list of :obj:`sale views <stoqlib.domain.sale.SaleView>`
        tied with the current client
        """
        from stoqlib.domain.sale import SaleView
        return self.store.find(SaleView,
                   SaleView.client_id == self.id).order_by(SaleView.open_date)

    def get_client_services(self):
        """Returns a list of sold
        :obj:`service views stoqlib.domain.sale.SoldServicesView>` with
        services consumed by this client
        """
        from stoqlib.domain.sale import SoldServicesView

        return self.store.find(SoldServicesView,
                               client_id=self.id).order_by(SoldServicesView.estimated_fix_date)

    def get_client_products(self):
        """Returns a list of products from SoldProductsView with products
        sold to the client
        """
        from stoqlib.domain.sale import SoldProductsView
        return self.store.find(SoldProductsView, client_id=self.id)

    def get_client_payments(self):
        """Returns a list of payment from InPaymentView with client's payments
        """
        from stoqlib.domain.payment.views import InPaymentView
        return self.store.find(InPaymentView,
                               person_id=self.person_id).order_by(InPaymentView.due_date)

    def get_last_purchase_date(self):
        """Fetch the date of the last purchased item by this client.
        None is returned if there are no sales yet made by the client

        :returns: the date of the last purchased item
        """
        from stoqlib.domain.sale import Sale
        max_date = self.get_client_sales().max(Sale.open_date)
        if max_date:
            return max_date.date()

    @property
    def remaining_store_credit(self):
        from stoqlib.domain.payment.views import InPaymentView
        status_query = Or(InPaymentView.status == Payment.STATUS_PENDING,
                          InPaymentView.status == Payment.STATUS_CONFIRMED)
        query = And(InPaymentView.person_id == self.person.id,
                    status_query,
                    InPaymentView.method_name == u'store_credit')

        debit = self.store.find(InPaymentView, query).sum(InPaymentView.value) or currency('0.0')
        return currency(self.credit_limit - debit)

    def _set_salary(self, value):
        assert value >= 0

        self._salary = value

        store = self.store
        salary_percentage = sysparam(store).CREDIT_LIMIT_SALARY_PERCENT

        if salary_percentage > 0:
            self.credit_limit = value * salary_percentage / 100

    def _get_salary(self):
        return self._salary

    salary = property(_get_salary, _set_salary)

    def can_purchase(self, method, total_amount):
        """This method checks the following to see if the client can
        purchase::

            - The parameter LATE_PAYMENTS_POLICY,
            - The payment method to be used,
            - The total amount of the |payment|,
            - The :obj:`.remaining_store_credit` of this client, when necessary.

        :param method: an |paymentmethod|.
        :param total_amount: the value of the |payment| that should be created
          for this client.
        :returns: ``True`` if user is allowed. Raises an SellError if user is not
          allowed to purchase.
        """
        from stoqlib.domain.payment.views import InPaymentView

        if (method.method_name == u'store_credit' and
            self.remaining_store_credit < total_amount):
            raise SellError(_(u'Client %s does not have enough credit '
                                u'left to purchase.') % self.person.name)

        # Client does not have late payments
        if not InPaymentView.has_late_payments(self.store,
                                               self.person):
            return True

        param = sysparam(self.store).LATE_PAYMENTS_POLICY
        if param == LatePaymentPolicy.ALLOW_SALES:
            return True
        elif param == LatePaymentPolicy.DISALLOW_SALES:
            raise SellError(_(u'It is not possible to sell for clients with '
                               u'late payments.'))
        elif (param == LatePaymentPolicy.DISALLOW_STORE_CREDIT
              and method.method_name == u'store_credit'):
            raise SellError(_(u'It is not possible to sell with store credit '
                               u'for clients with late payments.'))

        return True


class Supplier(Domain):
    """A company or an individual that produces, provides, or furnishes
    an item or service

    """

    implements(IActive, IDescribable)

    __storm_table__ = 'supplier'

    (STATUS_ACTIVE,
     STATUS_INACTIVE,
     STATUS_BLOCKED) = range(3)

    statuses = {STATUS_ACTIVE: _(u'Active'),
                STATUS_INACTIVE: _(u'Inactive'),
                STATUS_BLOCKED: _(u'Blocked')}

    person_id = IntCol()

    #: the |person|
    person = Reference(person_id, 'Person.id')

    #: active/inactive/blocked
    status = IntCol(default=STATUS_ACTIVE)

    #: A short description telling which products this supplier produces
    product_desc = UnicodeCol(default=u'')

    is_active = BoolCol(default=True)

    #
    # IActive
    #

    def inactivate(self):
        assert self.is_active, (u'This supplier is already inactive')
        self.is_active = False

    def activate(self):
        assert not self.is_active, (u'This supplier is already active')
        self.is_active = True

    def get_status_string(self):
        if self.is_active:
            return _(u'Active')
        return _(u'Inactive')

    #
    # IDescribable
    #

    def get_description(self):
        return self.person.name

    #
    # Public API
    #

    def get_name(self):
        """
        :returns: the supplier's name
        """
        return self.person.name

    @classmethod
    def get_active_suppliers(cls, store):
        query = And(cls.status == cls.STATUS_ACTIVE,
                    cls.person_id == Person.id)
        return store.find(cls, query).order_by(Person.name)

    def get_supplier_purchases(self):
        """
        Gets a list of PurchaseOrderViews representing all purchases done from
        this supplier.
        :returns: a list of PurchaseOrderViews.
        """
        from stoqlib.domain.purchase import PurchaseOrderView
        return self.store.find(PurchaseOrderView,
                    supplier_id=self.id).order_by(PurchaseOrderView.open_date)

    def get_last_purchase_date(self):
        """Fetch the date of the last purchased item by this supplier.
        ``None`` is returned if there are no sales yet made by the client.

        :returns: the date of the last purchased item
        :rtype: datetime.date or ``None``
        """
        orders = self.get_supplier_purchases()
        if orders.count():
            # The get_client_sales method already returns a sorted list of
            # sales by open_date column
            # pylint: disable=E1101
            return orders.last().open_date.date()
            # pylint: enable=E1101


class Employee(Domain):
    """An individual who performs work for an employer under a verbal
    or written understanding where the employer gives direction as to
    what tasks are done
    """

    implements(IActive, IDescribable)

    __storm_table__ = 'employee'

    (STATUS_NORMAL,
     STATUS_AWAY,
     STATUS_VACATION,
     STATUS_OFF) = range(4)

    statuses = {STATUS_NORMAL: _(u'Normal'),
                STATUS_AWAY: _(u'Away'),
                STATUS_VACATION: _(u'Vacation'),
                STATUS_OFF: _(u'Off')}

    #: normal/away/vacation/off
    status = IntCol(default=STATUS_NORMAL)

    person_id = IntCol()

    #: the |person|
    person = Reference(person_id, 'Person.id')

    #: salary for this employee
    salary = PriceCol(default=0)

    #: when this employeer started working for the branch
    admission_date = DateTimeCol(default=None)

    #: when the vaction expires for this employee
    expire_vacation = DateTimeCol(default=None)

    registry_number = UnicodeCol(default=None)
    education_level = UnicodeCol(default=None)
    dependent_person_number = IntCol(default=None)

    role_id = IntCol()

    #: A reference to an employee role object
    role = Reference(role_id, 'EmployeeRole.id')
    is_active = BoolCol(default=True)

    # This is Brazil-specific information
    workpermit_data_id = IntCol(default=None)
    workpermit_data = Reference(workpermit_data_id, 'WorkPermitData.id')
    military_data_id = IntCol(default=None)
    military_data = Reference(military_data_id, 'MilitaryData.id')
    voter_data_id = IntCol(default=None)
    voter_data = Reference(voter_data_id, 'VoterData.id')
    bank_account_id = IntCol(default=None)
    bank_account = Reference(bank_account_id, 'BankAccount.id')

    branch_id = IntCol()

    #: The branch this employee works on
    branch = Reference(branch_id, 'Branch.id')

    #
    # IActive
    #

    def inactivate(self):
        assert self.is_active, (u'This employee is already inactive')
        self.is_active = False

    def activate(self):
        assert not self.is_active, (u'This employee is already active')
        self.is_active = True

    def get_status_string(self):
        if self.is_active:
            return _(u'Active')
        return _(u'Inactive')

    #
    # IDescribable
    #

    def get_description(self):
        return self.person.name

    #
    # Public API
    #

    def get_role_history(self):
        return self.store.find(EmployeeRoleHistory,
                               employee=self)

    def get_active_role_history(self):
        store = self.store
        return store.find(EmployeeRoleHistory, employee=self,
                          is_active=True).one()

    @classmethod
    def get_active_employees(cls, store):
        """Return a list of active employees."""
        return store.find(cls,
            And(cls.status == cls.STATUS_NORMAL,
                cls.is_active == True))


class LoginUser(Domain):
    """A user that us able to login to the system
    """

    implements(IActive, IDescribable)

    __storm_table__ = 'login_user'

    (STATUS_ACTIVE,
     STATUS_INACTIVE) = range(2)

    statuses = {STATUS_ACTIVE: _(u'Active'),
                STATUS_INACTIVE: _(u'Inactive')}

    person_id = IntCol()

    #: the |person|
    person = Reference(person_id, 'Person.id')

    #: username, used to login it to the system
    username = UnicodeCol()

    #: a hash (md5) for the user password
    pw_hash = UnicodeCol()

    profile_id = IntCol()

    #: A profile represents a colection of information
    #: which represents what this user can do in the system
    profile = Reference(profile_id, 'UserProfile.id')

    is_active = BoolCol(default=True)

    def __init__(self, store=None, **kw):
        if 'password' in kw:
            kw['pw_hash'] = unicode(hashlib.md5(kw['password'] or u'').hexdigest())
            del kw['password']
        Domain.__init__(self, store=store, **kw)

    #
    # IActive
    #

    def inactivate(self):
        assert self.is_active, (u'This user is already inactive')
        self.is_active = False

    def activate(self):
        assert not self.is_active, (u'This user is already active')
        self.is_active = True

    def get_status_string(self):
        if self.is_active:
            return _(u'Active')
        return _(u'Inactive')

    #
    # IDescribable
    #

    def get_description(self):
        return self.person.name

    #
    # Public API
    #

    @classmethod
    def get_status_str(self):
        """Returns the status description of a user"""
        if self.is_active:
            return self.statuses[self.STATUS_ACTIVE]
        return self.statuses[self.STATUS_INACTIVE]

    @classmethod
    def get_active_users(cls, store):
        """Returns a list of all active |loginuser|s"""
        return store.find(cls, is_active=True)

    def get_associated_branches(self):
        """ Returns all the branches which the user has access
        """
        return self.store.find(UserBranchAccess,
                               user=self)

    def add_access_to(self, branch):
        UserBranchAccess(store=self.store, user=self, branch=branch)

    def has_access_to(self, branch):
        """ Checks if the user has access to the given branch.

        If the user has access to Administrative App, he has access to any
        branch.
        """
        if self.profile.check_app_permission(u'admin'):
            return True
        return UserBranchAccess.has_access(self.store, self, branch)

    def set_password(self, password):
        """Changes the user password.
        """
        self.pw_hash = unicode(hashlib.md5(password or u'').hexdigest())

    def login(self):
        station = get_current_station(self.store)
        if station:
            Event.log(Event.TYPE_USER,
                _(u"User '%s' logged in on '%s'") % (self.username,
                                                    station.name))
        else:
            Event.log(Event.TYPE_USER,
                _(u"User '%s' logged in") % (self.username, ))

    def logout(self):
        station = get_current_station(self.store)
        if station:
            Event.log(Event.TYPE_USER,
                _(u"User '%s' logged out from '%s'") % (self.username,
                                                       station.name))
        else:
            Event.log(Event.TYPE_USER,
                _(u"User '%s' logged out") % (self.username, ))


class Branch(Domain):
    """An administrative division of some larger or more complex
    organization
    """

    implements(IActive, IDescribable)

    __storm_table__ = 'branch'

    (STATUS_ACTIVE,
     STATUS_INACTIVE) = range(2)

    statuses = {STATUS_ACTIVE: _(u'Active'),
                STATUS_INACTIVE: _(u'Inactive')}

    person_id = IntCol()

    #: the |person|
    person = Reference(person_id, 'Person.id')

    manager_id = IntCol(default=None)

    #: An employee which is in charge of this branch
    manager = Reference(manager_id, 'Employee.id')

    is_active = BoolCol(default=True)

    #: Brazil specific, "Código de Regime Tributário", one of:
    #:
    #: * Simples Nacional
    #: * Simples Nacional – excesso de sublimite da receita bruta
    #: * Regime Normal
    crt = IntCol(default=1)

    #: An acronym that uniquely describes a branch
    acronym = UnicodeCol(default=None)

    #
    # IActive
    #

    def inactivate(self):
        assert self.is_active, (u'This branch is already inactive')
        self.is_active = False

    def activate(self):
        assert not self.is_active, (u'This branch is already active')
        self.is_active = True

    def get_status_string(self):
        if self.is_active:
            return _(u'Active')
        return _(u'Inactive')

    #
    # IDescribable
    #

    def get_description(self):
        person = self.person
        return person.company.fancy_name or person.name

    #
    # Public API
    #

    def set_acronym(self, value):
        """Sets the branch acronym.

        :param value: The new acronym for this branch. If an empty string is
          used, it will be changed to ``None``.
        """
        if value == u'':
            value = None

        self.acronym = value

    def check_acronym_exists(self, acronym):
        """Returns ``True`` if we already have a Company with the given acronym
        in the database.
        """
        return self.check_unique_value_exists(Branch.acronym, acronym)

    def is_from_same_company(self, other_branch):
        """Receives a branch and checks, using this and the other branch's
        cnpj, whether they are from the same company

        :param other_branch: an :class:`branch <Branch>`
        :returns: true if they are from same company, false otherwise
        """
        cnpj = self.person.company.cnpj
        other_cnpj = other_branch.person.company.cnpj

        if not cnpj or not other_cnpj:
            return False

        return cnpj.split(u'/')[0] == other_cnpj.split(u'/')[0]

    # Event

    def on_create(self):
        Event.log(Event.TYPE_SYSTEM,
                  _(u"Created branch '%s'") % (self.person.name, ))

    # Classmethods

    @classmethod
    def get_active_branches(cls, store):
        return store.find(cls, cls.is_active == True)


class SalesPerson(Domain):
    """An employee in charge of making sales

    """

    implements(IActive, IDescribable)

    __storm_table__ = 'sales_person'

    # Not really used right now
    (COMMISSION_GLOBAL,
     COMMISSION_BY_SALESPERSON,
     COMMISSION_BY_SELLABLE,
     COMMISSION_BY_PAYMENT_METHOD,
     COMMISSION_BY_BASE_SELLABLE_CATEGORY,
     COMMISSION_BY_SELLABLE_CATEGORY,
     COMMISSION_BY_SALE_TOTAL) = range(7)

    comission_types = {COMMISSION_GLOBAL: _(u'Globally'),
                       COMMISSION_BY_SALESPERSON: _(u'By Salesperson'),
                       COMMISSION_BY_SELLABLE: _(u'By Sellable'),
                       COMMISSION_BY_PAYMENT_METHOD: _(u'By Payment Method'),
                       COMMISSION_BY_BASE_SELLABLE_CATEGORY: _(u'By Base '
                                                              u'Sellable '
                                                              u'Category'),
                       COMMISSION_BY_SELLABLE_CATEGORY: _(u'By Sellable '
                                                         u'Category'),
                       COMMISSION_BY_SALE_TOTAL: _(u'By Sale Total')}

    person_id = IntCol()

    #: the |person|
    person = Reference(person_id, 'Person.id')

    #: The percentege of commission the company must pay
    #: for this salesman
    comission = PercentCol(default=0)

    #: A rule used to calculate the amount of
    #: commission. This is a reference to another object
    comission_type = IntCol(default=COMMISSION_BY_SALESPERSON)

    is_active = BoolCol(default=True)

    #
    # IActive
    #

    def inactivate(self):
        assert self.is_active, (u'This sales person is already inactive')
        self.is_active = False

    def activate(self):
        assert not self.is_active, (u'This sales person is already active')
        self.is_active = True

    def get_status_string(self):
        if self.is_active:
            return _(u'Active')
        return _(u'Inactive')

    #
    # IDescribable
    #

    def get_description(self):
        return self.person.name

    #
    # Public API
    #

    @classmethod
    def get_active_salespersons(cls, store):
        """Get a list of all active salespersons"""
        query = cls.is_active == True
        return store.find(cls, query)


class Transporter(Domain):
    """An individual or company engaged in the transportation
    """

    implements(IActive, IDescribable)

    __storm_table__ = 'transporter'

    person_id = IntCol()

    #: the |person|
    person = Reference(person_id, 'Person.id')

    is_active = BoolCol(default=True)

    #: The date when we start working with this transporter
    open_contract_date = DateTimeCol(default_factory=datetime.datetime.now)

    #FIXME: not used in purchases.
    #: The percentage amount of freight charged by this transporter
    freight_percentage = PercentCol(default=0)

    #
    # IActive
    #

    def inactivate(self):
        assert self.is_active, (u'This transporter is already inactive')
        self.is_active = False

    def activate(self):
        assert not self.is_active, (u'This transporter is already active')
        self.is_active = True

    def get_status_string(self):
        if self.is_active:
            return _(u'Active')
        return _(u'Inactive')

    #
    # IDescribable
    #

    def get_description(self):
        return self.person.name

    #
    # Public API
    #

    @classmethod
    def get_active_transporters(cls, store):
        """Get a list of all available transporters"""
        query = cls.is_active == True
        return store.find(cls, query)


class EmployeeRoleHistory(Domain):
    """Base class to store the employee role history."""

    __storm_table__ = 'employee_role_history'

    began = DateTimeCol(default_factory=datetime.datetime.now)
    ended = DateTimeCol(default=None)
    salary = PriceCol()
    role_id = IntCol()
    role = Reference(role_id, 'EmployeeRole.id')
    employee_id = IntCol()
    employee = Reference(employee_id, 'Employee.id')
    is_active = BoolCol(default=True)


class ClientSalaryHistory(Domain):
    """A class to keep track of all the salaries a client has had
    """

    __storm_table__ = 'client_salary_history'

    #: date when salary has been updated
    date = DateTimeCol()

    #: value of the updated salary
    new_salary = PriceCol()

    #: value of the previous salary
    old_salary = PriceCol()

    client_id = IntCol()

    #: the |client| whose salary is being stored
    client = Reference(client_id, 'Client.id')

    user_id = IntCol()

    #: the |loginuser| who updated the salary
    user = Reference(user_id, 'LoginUser.id')

    @classmethod
    def add(cls, store, old_salary, client, user):
        if old_salary != client.salary:
            ClientSalaryHistory(store=store,
                                date=datetime.date.today(),
                                new_salary=client.salary,
                                old_salary=old_salary,
                                client=client,
                                user=user)


class UserBranchAccess(Domain):
    """This class associates a |loginuser| to a |branch|.

    Users will only be able to login into Stoq if it is associated with the
    computer's branch.
    """

    __storm_table__ = 'user_branch_access'

    user_id = IntCol()

    #: the |loginuser|
    user = Reference(user_id, 'LoginUser.id')

    branch_id = IntCol()

    #: the |branch|
    branch = Reference(branch_id, 'Branch.id')

    @classmethod
    def has_access(cls, store, user, branch):
        """Checks if the given user has access to the given branch
        """
        return store.find(cls, user=user, branch=branch).one() is not None


#
# Views
#


class ClientView(Viewable):
    """Stores information about clients.

    Available fields are:
    :attribute id: id of the client table
    :attribute name: client name
    :attribute status: client financial status
    :attribute cpf: brazil-specific cpf attribute
    :attribute rg: brazil-specific rg_number attribute
    :attribute phone_number: client phone_number
    :attribute mobile_number: client mobile_number
    """

    implements(IDescribable)

    client = Client
    person = Person
    category = ClientCategory

    id = Client.id
    name = Person.name
    person_id = Person.id
    fancy_name = Company.fancy_name
    phone_number = Person.phone_number
    mobile_number = Person.mobile_number
    status = Client.status
    cnpj = Company.cnpj
    cpf = Individual.cpf
    birth_date = Individual.birth_date
    rg_number = Individual.rg_number
    client_category = ClientCategory.name

    tables = [
        Client,
        Join(Person,
                   Person.id == Client.person_id),
        LeftJoin(Individual,
                   Person.id == Individual.person_id),
        LeftJoin(Company,
                   Person.id == Company.person_id),
        LeftJoin(ClientCategory,
                   Client.category_id == ClientCategory.id),
        ]

    #
    # IDescribable
    #

    def get_description(self):
        return self.name + (self.fancy_name
                            and u" (%s)" % self.fancy_name or u"")

    #
    # Public API
    #

    @property
    def cnpj_or_cpf(self):
        return self.cnpj or self.cpf

    @classmethod
    def get_active_clients(cls, store):
        """Return a list of active clients.
        An active client is a person who are authorized to make new sales
        """
        return store.find(cls, status=Client.STATUS_SOLVENT).order_by(cls.name)


class EmployeeView(Viewable):

    implements(IDescribable)

    employee = Employee

    id = Employee.id
    person_id = Person.id
    name = Person.name
    role = EmployeeRole.name
    status = Employee.status
    is_active = Employee.is_active
    registry_number = Employee.registry_number

    tables = [
        Employee,
        Join(Person, Person.id == Employee.person_id),
        Join(EmployeeRole, Employee.role_id == EmployeeRole.id),
        ]

    #
    # IDescribable
    #

    def get_description(self):
        return self.name

    #
    # Public API
    #

    def get_status_string(self):
        return Employee.statuses[self.status]

    @classmethod
    def get_active_employees(cls, store):
        """Return a list of active employees."""
        return store.find(cls, status=Employee.STATUS_NORMAL,
                          is_active=True)


class SupplierView(Viewable):

    implements(IDescribable)

    supplier = Supplier

    id = Supplier.id
    person_id = Person.id
    name = Person.name
    phone_number = Person.phone_number
    fancy_name = Company.fancy_name
    cnpj = Company.cnpj
    status = Supplier.status

    tables = [
        Supplier,
        Join(Person,
                   Person.id == Supplier.person_id),
        LeftJoin(Company,
                   Person.id == Company.person_id),
        ]

    #
    # IDescribable
    #

    def get_description(self):
        return self.name

    #
    # Public API
    #

    def get_status_string(self):
        return Supplier.statuses[self.status]


class TransporterView(Viewable):
    """
    Stores information about transporters

    :cvar id: the id of transporter table
    :cvar name: the transporter name
    :cvar phone_number: the transporter phone number
    :cvar person_id: the id of person table
    :cvar status: the current status of the transporter
    :cvar freight_percentage: the freight percentage charged
    """

    implements(IDescribable)

    transporter = Transporter

    id = Transporter.id
    person_id = Person.id
    name = Person.name
    phone_number = Person.phone_number
    freight_percentage = Transporter.freight_percentage
    is_active = Transporter.is_active

    tables = [
        Transporter,
        Join(Person, Person.id == Transporter.person_id),
        ]

    #
    # IDescribable
    #

    def get_description(self):
        return self.name


class BranchView(Viewable):
    implements(IDescribable)

    Manager_Person = ClassAlias(Person, 'person_manager')

    branch = Branch

    id = Branch.id
    acronym = Branch.acronym
    is_active = Branch.is_active
    person_id = Person.id
    name = Person.name
    phone_number = Person.phone_number
    manager_name = Manager_Person.name

    tables = [
        Branch,
        Join(Person, Person.id == Branch.person_id),
        LeftJoin(Employee, Branch.manager_id == Employee.id),
        LeftJoin(Manager_Person, Employee.person_id == Manager_Person.id),
        ]

    #
    # IDescribable
    #

    def get_description(self):
        return self.name

    #
    # Public API
    #

    def get_status_str(self):
        if self.is_active:
            return _(u'Active')

        return _(u'Inactive')


class UserView(Viewable):
    """
    Retrieves information about user in the system.

    :cvar id: the id of user table
    :cvar name: the user full name
    :cvar is_active: the current status of the transporter
    :cvar username: the username (login)
    :cvar person_id: the id of person table
    :cvar profile_id: the id of the user profile
    :cvar profile_name: the name of the user profile (eg: Salesperson)
    """

    implements(IDescribable)

    user = LoginUser

    id = LoginUser.id
    person_id = Person.id
    name = Person.name
    is_active = LoginUser.is_active
    username = LoginUser.username
    profile_id = LoginUser.profile_id
    profile_name = UserProfile.name

    tables = [
        LoginUser,
        Join(Person, Person.id == LoginUser.person_id),
        LeftJoin(UserProfile, LoginUser.profile_id == UserProfile.id),
        ]

    #
    # IDescribable
    #

    def get_description(self):
        return self.name

    #
    # Public API
    #

    def get_status_str(self):
        if self.is_active:
            return _(u'Active')

        return _(u'Inactive')


class CreditCheckHistoryView(Viewable):
    """
    """

    User_Person = ClassAlias(Person, 'user_person')

    check_history = CreditCheckHistory

    id = CreditCheckHistory.id
    _person_id = Person.id
    client_name = Person.name
    check_date = CreditCheckHistory.check_date
    identifier = CreditCheckHistory.identifier
    status = CreditCheckHistory.status
    notes = CreditCheckHistory.notes
    user = User_Person.name

    tables = [
        CreditCheckHistory,
        LeftJoin(Client, Client.id == CreditCheckHistory.client_id),
        LeftJoin(Person, Person.id == Client.person_id),
        LeftJoin(LoginUser, LoginUser.id == CreditCheckHistory.user_id),
        LeftJoin(User_Person, LoginUser.person_id == User_Person.id),
    ]

    #
    # Public API
    #

    @classmethod
    def find_by_client(cls, store, client):
        if client:
            return store.find(cls, CreditCheckHistory.client == client)
        return store.find(cls)


class CallsView(Viewable):
    """Store information about the realized calls to client.
    """

    implements(IDescribable)

    Attendant_Person = ClassAlias(Person, 'attendant_person')

    call = Calls
    person = Person

    id = Calls.id
    person_id = Person.id
    name = Person.name
    date = Calls.date
    description = Calls.description
    message = Calls.message
    attendant = Attendant_Person.name

    tables = [
        Calls,
        LeftJoin(Person, Person.id == Calls.person_id),
        LeftJoin(LoginUser, LoginUser.id == Calls.attendant_id),
        LeftJoin(Attendant_Person, LoginUser.person_id == Attendant_Person.id),
        ]

    #
    # IDescribable
    #

    def get_description(self):
        return self.description

    #
    # Public API
    #

    @classmethod
    def find_by_client_date(cls, store, client, date):
        queries = []
        if client:
            queries.append(Calls.person == client)

        if date:
            if isinstance(date, tuple):
                date_query = And(Date(Calls.date) >= date[0],
                                 Date(Calls.date) <= date[1])
            else:
                date_query = Date(Calls.date) == date

            queries.append(date_query)

        if queries:
            return store.find(cls, And(*queries))

        return store.find(cls)

    @classmethod
    def find_by_date(cls, store, date):
        return cls.find_by_client_date(store, None, date)


class ClientCallsView(CallsView):
    tables = CallsView.tables[:]
    tables.append(
        Join(Client, Client.person_id == Person.id))


class ClientSalaryHistoryView(Viewable):
    """Store information about a client's salary history
    """

    id = ClientSalaryHistory.id
    date = ClientSalaryHistory.date
    new_salary = ClientSalaryHistory.new_salary
    user = Person.name

    tables = [
        ClientSalaryHistory,
        LeftJoin(LoginUser, LoginUser.id == ClientSalaryHistory.user_id),
        LeftJoin(Person, LoginUser.person_id == Person.id),
        ]

    @classmethod
    def find_by_client(cls, store, client):
        if client:
            return store.find(cls, ClientSalaryHistory.client == client)

        return store.find(cls)
