# -*- 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>
##
"""Payment methods"""
# pylint: enable=E1101
import operator
from storm.expr import And
from storm.references import Reference
from zope.interface import implementer
from stoqlib.database.expr import TransactionTimestamp
from stoqlib.database.properties import IntCol, BoolCol, UnicodeCol, IdCol
from stoqlib.database.properties import PercentCol
from stoqlib.domain.base import Domain
from stoqlib.domain.interfaces import IActive, IDescribable
from stoqlib.domain.payment.payment import Payment
from stoqlib.exceptions import DatabaseInconsistency, PaymentMethodError
from stoqlib.lib.payment import generate_payments_values
from stoqlib.lib.translation import locale_sorted, stoqlib_gettext
_ = stoqlib_gettext
#
# Domain Classes
#
[docs]class CheckData(Domain):
"""Stores check informations and also a history of possible
devolutions.
"""
__storm_table__ = 'check_data'
payment_id = IdCol()
#: the :class:`payment <stoqlib.domain.payment.Payment>`
payment = Reference(payment_id, 'Payment.id')
bank_account_id = IdCol()
#: the :class:`bank account <stoqlib.domain.account.BankAccount>`
bank_account = Reference(bank_account_id, 'BankAccount.id')
@implementer(IActive)
@implementer(IDescribable)
[docs]class PaymentMethod(Domain):
"""A PaymentMethod controls how a payments is paid. Example of payment
methods are::
* money
* bill
* check
* credit card
This class consists of the persistent part of a payment method.
The logic itself for the various different methods are in the
PaymentMethodOperation classes. Each :class:`PaymentMethod` has a
PaymentMethodOperation associated.
"""
__storm_table__ = 'payment_method'
method_name = UnicodeCol()
is_active = BoolCol(default=True)
daily_interest = PercentCol(default=0)
#: a value for the penalty. It must always be in the format::
#:
#: 0 <= penalty <= 100
#:
penalty = PercentCol(default=0)
#: which day in the month is the credit provider going to pay the store?
#: Usually they pay in the same day every month.
payment_day = IntCol(default=None)
#: which day the credit provider stoq counting sales to pay in the
#: payment_day? Sales after this day will be paid only in the next month.
closing_day = IntCol(default=None)
max_installments = IntCol(default=1)
destination_account_id = IdCol(default=None)
destination_account = Reference(destination_account_id, 'Account.id')
#
# IActive implementation
#
def inactivate(self):
assert self.is_active, ('This provider is already inactive')
self.is_active = False
def activate(self):
assert not self.is_active, ('This provider is already active')
self.is_active = True
def get_status_string(self):
if self.is_active:
return _(u'Active')
return _(u'Inactive')
#
# IDescribable implementation
#
def get_description(self):
return self.description
#
# Properties
#
@property
def description(self):
return self.operation.description
@property
def operation(self):
"""Get the operation for this method.
The operation contains method specific logic when
creating/deleting a payment.
:return: the operation associated with the method
:rtype: object implementing IPaymentOperation
"""
from stoqlib.domain.payment.operation import get_payment_operation
return get_payment_operation(self.method_name)
#
# Public API
#
# FIXME All create_* methods should be moved to a separate class,
# they don't really belong to the method itself.
# They should either go into the group or to a separate payment
# factory singleton.
[docs] def create_payment(self, payment_type, payment_group, branch, value,
due_date=None, description=None, base_value=None,
payment_number=None, identifier=None):
"""Creates a new payment according to a payment method interface
:param payment_type: the kind of payment, in or out
:param payment_group: a :class:`PaymentGroup` subclass
:param branch: the :class:`branch <stoqlib.domain.person.Branch>`
associated with the payment, for incoming payments this is the
branch receiving the payment and for outgoing payments this is the
branch sending the payment.
:param value: value of payment
:param due_date: optional, due date of payment
:param details: optional
:param description: optional, description of the payment
:param base_value: optional
:param payment_number: optional
:returns: a :class:`payment <stoqlib.domain.payment.Payment>`
"""
store = self.store
if due_date is None:
due_date = TransactionTimestamp()
if payment_type == Payment.TYPE_IN:
query = And(Payment.group_id == payment_group.id,
Payment.method_id == self.id,
Payment.payment_type == Payment.TYPE_IN,
Payment.status != Payment.STATUS_CANCELLED)
payment_count = store.find(Payment, query).count()
if payment_count == self.max_installments:
raise PaymentMethodError(
_('You can not create more inpayments for this payment '
'group since the maximum allowed for this payment '
'method is %d') % self.max_installments)
elif payment_count > self.max_installments:
raise DatabaseInconsistency(
_('You have more inpayments in database than the maximum '
'allowed for this payment method'))
if not description:
description = self.describe_payment(payment_group)
payment = Payment(store=store,
identifier=identifier,
branch=branch,
payment_type=payment_type,
due_date=due_date,
value=value,
base_value=base_value,
group=payment_group,
method=self,
category=None,
description=description,
payment_number=payment_number)
self.operation.payment_create(payment)
return payment
[docs] def create_payments(self, payment_type, group, branch, value, due_dates):
"""Creates new payments
The values of the individual payments are calculated by taking
the value and dividing it by the number of payments.
The number of payments is determined by the length of the due_dates
sequence.
:param payment_type: the kind of payment, in or out
:param payment_group: a |paymentgroup|
:param branch: the |branch| associated with the payments, for incoming
payments this is the branch receiving the payment and for outgoing
payments this is the branch sending the payment.
:param value: total value of all payments
:param due_dates: a list of datetime objects
:returns: a list of |payments|
"""
installments = len(due_dates)
if not installments:
raise ValueError(_('Need at least one installment'))
if payment_type == Payment.TYPE_IN:
if installments > self.max_installments:
raise ValueError(
_('The number of installments can not be greater than %d '
'for payment method %s') % (self.max_installments,
self.method_name))
# Create the requested payments with the right:
# - due_date
# - description for the specific group
# - normalized value
payments = []
normalized_values = generate_payments_values(value, installments)
for (i, due_date), normalized_value in zip(enumerate(due_dates),
normalized_values):
description = self.describe_payment(group, i + 1, installments)
payment = self.create_payment(
payment_type=payment_type,
payment_group=group,
branch=branch,
value=normalized_value,
due_date=due_date,
description=description)
payments.append(payment)
return payments
[docs] def describe_payment(self, payment_group, installment=1, installments=1):
""" Returns a string describing payment, in the following
format: current_installment/total_of_installments payment_description
for payment_group_description
:param payment_group: a :class:`PaymentGroup`
:param installment: current installment
:param installments: total installments
:returns: a payment description
"""
assert installment > 0
assert installments > 0
assert installments >= installment
# TRANSLATORS: This will generate something like: 1/1 Money for sale 00001
return _(u'{installment} {method_name} for {order_description}').format(
installment=u'%s/%s' % (installment, installments),
method_name=self.get_description(),
order_description=payment_group.get_description())
@classmethod
[docs] def get_active_methods(cls, store):
"""Returns a list of active payment methods
"""
methods = store.find(PaymentMethod, is_active=True)
return locale_sorted(methods,
key=operator.attrgetter('description'))
@classmethod
[docs] def get_by_name(cls, store, name):
"""Returns the Payment method associated by the nmae
:param name: name of a payment method
:returns: a :class:`payment methods <PaymentMethod>`
"""
return store.find(PaymentMethod, method_name=name).one()
@classmethod
[docs] def get_by_account(cls, store, account):
"""Returns the Payment method associated with an account
:param account: |account| for which the payment methods are
associated with
:returns: a sequence :class:`payment methods <PaymentMethod>`
"""
return store.find(PaymentMethod, destination_account=account)
@classmethod
[docs] def get_creatable_methods(cls, store, payment_type, separate):
"""Gets a list of methods that are creatable.
Eg, you can use them to create new payments.
:returns: a list of :class:`payment methods <PaymentMethod>`
"""
methods = []
for method in cls.get_active_methods(store):
if not method.operation.creatable(method, payment_type,
separate):
continue
methods.append(method)
return methods
@classmethod
[docs] def get_editable_methods(cls, store):
"""Gets a list of methods that are editable
Eg, you can change the details such as maximum installments etc.
:returns: a list of :class:`payment methods <PaymentMethod>`
"""
# FIXME: Dont let users see online payments for now, to avoid
# confusions with active state. online is an exception to that
# logic. 'trade' for the same reason
clause = And(cls.method_name != u'online',
cls.method_name != u'trade')
methods = store.find(cls, clause)
return locale_sorted(methods,
key=operator.attrgetter('description'))
[docs] def selectable(self):
"""Finds out if the method is selectable, eg
if the user can select it when doing a sale.
:returns: ``True`` if selectable
"""
return self.operation.selectable(self)