Source code for stoqlib.domain.payment.group

# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4

##
## Copyright (C) 2005-2013 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 groups, a set of payments

The five use cases for payment groups are:

  - Sale
  - Purchase
  - Renegotiation
  - Stockdecreae
  - Lonely payments

All of them contains a set of payments and they behaves slightly
differently
"""

# pylint: enable=E1101

from kiwi.currency import currency
from storm.expr import And, In, Not
from storm.references import Reference
from zope.interface import implementer

from stoqlib.database.properties import IdCol
from stoqlib.domain.base import Domain
from stoqlib.domain.events import PaymentGroupGetOrderEvent
from stoqlib.domain.interfaces import IContainer
from stoqlib.domain.payment.payment import Payment
from stoqlib.lib.translation import stoqlib_gettext

_ = stoqlib_gettext


@implementer(IContainer)
[docs]class PaymentGroup(Domain): """A set of |payments|, all related to the same |sale|, |purchase|, |paymentrenegotiation| or |stockdecrease|. The set of payments can also be lonely, eg not associated with one of objects mentioned above. A payer is paying the recipient who's receiving the |payments|. """ __storm_table__ = 'payment_group' payer_id = IdCol(default=None) #: the |person| who is paying this group payer = Reference(payer_id, 'Person.id') recipient_id = IdCol(default=None) #: the |person| who is receiving this group recipient = Reference(recipient_id, 'Person.id') # XXX: Rename to renegotiated renegotiation_id = IdCol(default=None) #: the payment renegotation this group belongs to renegotiation = Reference(renegotiation_id, 'PaymentRenegotiation.id') #: The |sale| if this group is part of one sale = Reference('id', 'Sale.group_id', on_remote=True) #: The |purchase| if this group is part of one purchase = Reference('id', 'PurchaseOrder.group_id', on_remote=True) #: the payment renegotation the |payments| of this group belongs to _renegotiation = Reference('id', 'PaymentRenegotiation.group_id', on_remote=True) #: The |stockdecrease| if this group is part of one stock_decrease = Reference('id', 'StockDecrease.group_id', on_remote=True) # # IContainer implementation # def add_item(self, payment): payment.group = self def remove_item(self, payment): assert payment.group == self, payment.group payment.group = None def get_items(self): store = self.store return store.find(Payment, group=self).order_by( Payment.due_date, Payment.identifier) # # Properties # @property def payments(self): """Returns all payments of this group :returns: a list of |payments| """ return self.get_items() @property def installments_number(self): """The number of installments(|payments|) that are part of this group.""" return self.payments.count() # # Private # def _get_paid_payments(self): return self.store.find(Payment, And(Payment.group_id == self.id, In(Payment.status, [Payment.STATUS_PAID, Payment.STATUS_REVIEWING, Payment.STATUS_CONFIRMED]))) def _get_preview_payments(self): return self.store.find(Payment, status=Payment.STATUS_PREVIEW, group=self) def _get_payments_sum(self, payments, attr): in_payments_value = payments.find( Payment.payment_type == Payment.TYPE_IN).sum(attr) or 0 out_payments_value = payments.find( Payment.payment_type == Payment.TYPE_OUT).sum(attr) or 0 if self.sale or self._renegotiation: return currency(in_payments_value - out_payments_value) elif self.purchase: return currency(out_payments_value - in_payments_value) # FIXME: Is this right for payments not linked to a # sale/purchase/renegotiation? return currency(payments.sum(attr) or 0) # # Public API #
[docs] def get_order_object(self): """Get the order object related to this payment group""" for obj in [self.sale, self.purchase, self._renegotiation, self.stock_decrease]: if obj is not None: return obj return PaymentGroupGetOrderEvent.emit(self, self.store)
[docs] def confirm(self): """Confirms all |payments| in this group Confirming the payment group means that the customer has confirmed the payments. All individual payments are set to pending. """ for payment in self._get_preview_payments(): payment.set_pending()
[docs] def pay(self): """Pay all |payments| in this group """ for payment in self.get_valid_payments(): if payment.is_paid(): continue payment.pay()
[docs] def pay_method_payments(self, method_name): """Pay all |payments| of a method in this group :param method_name: the method of the payments to be paid """ for payment in self.get_valid_payments(): if payment.is_of_method(method_name) and not payment.is_paid(): payment.pay()
[docs] def cancel(self): """Cancel all pending |payments| in this group """ for payment in self.get_pending_payments(): if not payment.is_cancelled(): payment.cancel()
[docs] def get_total_paid(self): """Returns the sum of all paid |payment| values within this group. :returns: the total paid value """ return self._get_payments_sum(self._get_paid_payments(), Payment.value)
[docs] def get_total_value(self): """Returns the sum of all |payment| values. This will consider all payments ignoring just the cancelled ones. If you want to ignore preview payments too, use :meth:`.get_total_confirmed_value` instead :returns: the total payment value or zero. """ return self._get_payments_sum(self.get_valid_payments(), Payment.value)
[docs] def get_total_to_pay(self): """Returns the total amount to be paid to have the group fully paid. """ payments = self.store.find( Payment, And(Payment.group_id == self.id, Payment.status == Payment.STATUS_PENDING)) return self._get_payments_sum(payments, Payment.value)
[docs] def get_total_confirmed_value(self): """Returns the sum of all confirmed payments values This will consider all payments ignoring cancelled and preview ones, that is, if a payment is confirmed/reviewing/paid it will be summed. If you want to consider the preview ones too, use :meth:`.get_total_value` instead :returns: the total confirmed payments value """ payments = self.store.find( Payment, And(Payment.group_id == self.id, Not(In(Payment.status, [Payment.STATUS_CANCELLED, Payment.STATUS_PREVIEW])))) return self._get_payments_sum(payments, Payment.value)
# FIXME: with proper database transactions we can probably remove this
[docs] def clear_unused(self): """Delete payments of preview status associated to the current payment_group. It can happen if user open and cancel this wizard. """ for payment in self._get_preview_payments(): self.remove_item(payment) payment.delete()
[docs] def get_description(self): """Returns a small description for the payment group which will be used in payment descriptions :returns: the description """ # FIXME: Now that we have a get_order_object, we can ask each of # those objects (Sale, PurchaseOrder, etc) to describe themselves # and remove those if/elifs bellow if self.sale: return _(u'sale %s') % self.sale.identifier elif self.purchase: return _(u'order %s') % self.purchase.identifier elif self._renegotiation: return _(u'renegotiation %s') % self._renegotiation.identifier elif self.stock_decrease: return _(u'stock decrease %s') % self.stock_decrease.identifier order_obj = self.get_order_object() # FIXME: Add a proper description when there's no order_obj return order_obj.payment_description if order_obj else u''
[docs] def get_pending_payments(self): """Returns a list of pending |payments| :returns: list of |payments| """ return self.store.find(Payment, group=self, status=Payment.STATUS_PENDING)
[docs] def get_parent(self): """Return the |sale|, |purchase|, |paymentrenegotiation| or |stockdecrease| this group is part of. :returns: the object this group is part of or ``None`` """ if self.sale: return self.sale elif self.purchase: return self.purchase elif self._renegotiation: return self._renegotiation elif self.stock_decrease: return self.stock_decrease return None
[docs] def get_total_discount(self): """Returns the sum of all |payment| discounts. :returns: the total payment discount or zero. """ return self._get_payments_sum(self.get_valid_payments(), Payment.discount)
[docs] def get_total_interest(self): """Returns the sum of all |payment| interests. :returns: the total payment interest or zero. """ return self._get_payments_sum(self.get_valid_payments(), Payment.interest)
[docs] def get_total_penalty(self): """Returns the sum of all |payment| penalties. :returns: the total payment penalty or zero. """ return self._get_payments_sum(self.get_valid_payments(), Payment.penalty)
[docs] def get_valid_payments(self): """Returns all |payments| that are not cancelled. :returns: list of |payments| """ return self.store.find(Payment, And(Payment.group_id == self.id, Payment.status != Payment.STATUS_CANCELLED))
[docs] def get_payments_by_method_name(self, method_name): """Returns all |payments| of a specific |paymentmethod| within this group. :param unicode method_name: the name of the method :returns: list of |payments| """ from stoqlib.domain.payment.method import PaymentMethod return self.store.find( Payment, And(Payment.group_id == self.id, Payment.method_id == PaymentMethod.id, PaymentMethod.method_name == method_name))