[POS-commit] r5446 - stoqlib/trunk/stoqlib/domain/payment
Johan Dahlin
jdahlin at async.com.br
Mon Dec 4 10:15:44 BRST 2006
Author: jdahlin
Date: Mon Dec 4 10:15:44 2006
New Revision: 5446
Added:
stoqlib/trunk/stoqlib/domain/payment/payment.py
Log:
Add payment.payment too
Added: stoqlib/trunk/stoqlib/domain/payment/payment.py
==============================================================================
--- (empty file)
+++ stoqlib/trunk/stoqlib/domain/payment/payment.py Mon Dec 4 10:15:44 2006
@@ -0,0 +1,518 @@
+# -*- coding: utf-8 -*-
+# vi:si:et:sw=4:sts=4:ts=4
+
+##
+## Copyright (C) 2005,2006 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): Evandro Vale Miquelito <evandro at async.com.br>
+## Henrique Romano <henrique at async.com.br>
+##
+""" Payment management implementations."""
+
+from datetime import datetime, date
+from decimal import Decimal
+
+from kiwi.argcheck import argcheck
+from kiwi.datatypes import currency
+from sqlobject import IntCol, DateTimeCol, UnicodeCol, ForeignKey
+from zope.interface import implements
+
+from stoqlib.database.runtime import get_current_branch
+from stoqlib.database.columns import PriceCol, AutoIncCol
+from stoqlib.exceptions import DatabaseInconsistency, StoqlibError
+from stoqlib.lib.translation import stoqlib_gettext
+from stoqlib.lib.parameters import sysparam
+from stoqlib.lib.defaults import (get_method_names, get_all_methods_dict,
+ METHOD_MONEY)
+from stoqlib.domain.fiscal import (IssBookEntry, IcmsIpiBookEntry,
+ AbstractFiscalBookEntry)
+from stoqlib.domain.base import Domain, ModelAdapter, InheritableModelAdapter
+from stoqlib.domain.payment.operation import PaymentOperation
+from stoqlib.domain.interfaces import (IInPayment, IOutPayment, IPaymentGroup,
+ ICheckPM, IBillPM, IContainer,
+ IPaymentDevolution, IPaymentDeposit)
+
+_ = stoqlib_gettext
+
+
+#
+# Domain Classes
+#
+
+
+class Payment(Domain):
+ """ The payment representation in Stoq.
+
+ B{Importante attributes}:
+ - I{interest}: the absolute value for the interest associated with
+ this payment.
+ - I{discount}: the absolute value for the discount associated with
+ this payment.
+ """
+
+ (STATUS_PREVIEW,
+ STATUS_PENDING,
+ STATUS_PAID,
+ STATUS_REVIEWING,
+ STATUS_CONFIRMED,
+ STATUS_CANCELLED) = range(6)
+
+ statuses = {STATUS_PREVIEW: _(u'Preview'),
+ STATUS_PENDING: _(u'To Pay'),
+ STATUS_PAID: _(u'Paid'),
+ STATUS_REVIEWING: _(u'Reviewing'),
+ STATUS_CONFIRMED: _(u'Confirmed'),
+ STATUS_CANCELLED: _(u'Cancelled')}
+
+ identifier = AutoIncCol('stoqlib_payment_identifier_seq')
+ status = IntCol(default=STATUS_PREVIEW)
+ open_date = DateTimeCol(default=datetime.now)
+ due_date = DateTimeCol()
+ paid_date = DateTimeCol(default=None)
+ cancel_date = DateTimeCol(default=None)
+ paid_value = PriceCol(default=0)
+ base_value = PriceCol()
+ value = PriceCol()
+ interest = PriceCol(default=0)
+ discount = PriceCol(default=0)
+ description = UnicodeCol(default=None)
+ payment_number = UnicodeCol(default=None)
+ method = ForeignKey('AbstractPaymentMethodAdapter')
+ # FIXME: Move to methods itself?
+ method_details = ForeignKey('PaymentMethodDetails', default=None)
+ group = ForeignKey('AbstractPaymentGroup')
+ till = ForeignKey('Till')
+ destination = ForeignKey('PaymentDestination')
+
+ def _check_status(self, status, operation_name):
+ assert self.status == status, ('Invalid status for %s '
+ 'operation: %s' % (operation_name,
+ self.statuses[self.status]))
+
+ #
+ # SQLObject hooks
+ #
+
+ def _create(self, id, **kw):
+ if not 'value' in kw:
+ raise TypeError('You must provide a value argument')
+ if not 'base_value' in kw or not kw['base_value']:
+ kw['base_value'] = kw['value']
+ Domain._create(self, id, **kw)
+
+ #
+ # Public API
+ #
+
+ def get_status_str(self):
+ if not self.statuses.has_key(self.status):
+ raise DatabaseInconsistency('Invalid status for Payment '
+ 'instance, got %d' % self.status)
+ return self.statuses[self.status]
+
+ def get_days_late(self):
+ days_late = datetime.today() - self.due_date
+ if days_late.days < 0:
+ return 0
+ else:
+ return days_late.days
+
+ def set_pending(self):
+ """Set a STATUS_PREVIEW payment as STATUS_PENDING. This also means
+ that this is valid payment and its owner actually can charge it
+ """
+ self._check_status(self.STATUS_PREVIEW, 'set_pending')
+ self.status = self.STATUS_PENDING
+
+ def pay(self, paid_date=None, paid_value=None):
+ """Pay the current payment set its status as STATUS_PAID"""
+ self._check_status(self.STATUS_PENDING, 'pay')
+ paid_value = paid_value or (self.value - self.discount +
+ self.interest)
+ self.paid_value = paid_value
+ self.paid_date = paid_date or datetime.now()
+ self.status = self.STATUS_PAID
+
+ def submit(self, submit_date=None):
+ """The first stage of payment acquittance is submiting and mark a
+ payment with STATUS_REVIEWING
+ """
+ self._check_status(self.STATUS_PAID, 'submit')
+ conn = self.get_connection()
+ operation = PaymentOperation(connection=conn,
+ operation_date=submit_date)
+ operation.addFacet(IPaymentDeposit, connection=conn)
+ self.status = self.STATUS_REVIEWING
+
+ def reject(self, reason, reject_date=None):
+ """If there is some problems in the first stage of payment
+ acquittance we must call reject for it.
+ """
+ self._check_status(self.STATUS_REVIEWING, 'reject')
+ conn = self.get_connection()
+ operation = PaymentOperation(connection=conn,
+ operation_date=reject_date)
+ operation.addFacet(IPaymentDevolution, connection=conn, reason=reason)
+ self.status = self.STATUS_PAID
+
+ def cancel_till_entry(self):
+ if self.status == Payment.STATUS_CANCELLED:
+ raise StoqlibError("This payment is already cancelled")
+ self._check_status(self.STATUS_PENDING, 'reverse selection')
+ self.status = self.STATUS_CANCELLED
+ payment = self.clone()
+ description = (_('Cancellation of payment number %s')
+ % self.identifier)
+ payment.description = description
+ payment.value *= -1
+ payment.due_date = datetime.now()
+
+ def cancel(self):
+ # TODO Check for till entries here and call cancel_till_entry if
+ # it's possible. Bug 2598
+ if self.status not in [Payment.STATUS_PREVIEW, Payment.STATUS_PENDING]:
+ raise StoqlibError("Invalid status for cancel operation, "
+ "got %s" % self.get_status_str())
+ self.status = self.STATUS_CANCELLED
+ self.cancel_date = datetime.now()
+
+ @argcheck(date)
+ def get_payable_value(self, paid_date=None):
+ """ Returns the calculated payment value with the daily penalty.
+ Note that the payment group daily_penalty must be
+ between 0 and 100.
+ """
+ if self.status in [self.STATUS_PREVIEW, self.STATUS_CANCELLED]:
+ return self.value
+ if self.status in [self.STATUS_PAID, self.STATUS_REVIEWING,
+ self.STATUS_CONFIRMED]:
+ return self.paid_value
+ if self.method.get_implemented_iface() in (ICheckPM, IBillPM):
+ paid_date = paid_date or datetime.now()
+ days = (paid_date - self.due_date).days
+ if days <= 0:
+ return self.value
+ daily_penalty = self.method.daily_penalty / 100
+ return self.value + days * (daily_penalty * self.value)
+ else:
+ return self.value
+
+ def get_thirdparty(self):
+ if self.method_details:
+ return self.method_details.get_thirdparty()
+ return self.method.get_thirdparty(self.group)
+
+ def get_thirdparty_name(self):
+ thirdparty = self.get_thirdparty()
+ if thirdparty:
+ return thirdparty.name
+ return _(u'Anonymous')
+
+
+class AbstractPaymentGroup(InheritableModelAdapter):
+ """A base class for payment group adapters. """
+
+ (STATUS_PREVIEW,
+ STATUS_OPEN,
+ STATUS_CLOSED,
+ STATUS_CANCELLED) = range(4)
+
+ statuses = {STATUS_PREVIEW: _(u"Preview"),
+ STATUS_OPEN: _(u"Open"),
+ STATUS_CLOSED: _(u"Closed"),
+ STATUS_CANCELLED: _(u"Cancelled")}
+
+ implements(IPaymentGroup, IContainer)
+
+ status = IntCol(default=STATUS_OPEN)
+ open_date = DateTimeCol(default=datetime.now)
+ close_date = DateTimeCol(default=None)
+ cancel_date = DateTimeCol(default=None)
+ default_method = IntCol(default=METHOD_MONEY)
+ installments_number = IntCol(default=1)
+ interval_type = IntCol(default=None)
+ intervals = IntCol(default=None)
+
+ def _create_till_entry(self, value, description, till):
+ from stoqlib.domain.till import TillEntry
+ conn = self.get_connection()
+ return TillEntry(connection=conn,
+ description=description, value=value, till=till,
+ payment_group=self)
+
+ #
+ # IPaymentGroup implementation
+ #
+
+ #
+ # FIXME: We should to remove all these methods without implementation, so
+ # we can ensure that interface are properly implemented in subclasses.
+ #
+ def get_thirdparty(self):
+ raise NotImplementedError
+
+ def get_group_description(self):
+ """Returns a small description for the payment group which will be
+ used in payment descriptions
+ """
+ raise NotImplementedError
+
+ def update_thirdparty_status(self):
+ raise NotImplementedError
+
+ def get_balance(self):
+ # FIXME: Move sum to SQL statement
+ return sum([s.value for s in self.get_items()])
+
+ def get_total_received(self):
+ # FIXME: Proper implementation
+ return currency(0)
+
+ def add_payment(self, value, description, method, destination=None,
+ due_date=None):
+ if due_date is None:
+ due_date = datetime.now()
+ """Create a new payment and add it to the group"""
+ from stoqlib.domain.till import Till
+ conn = self.get_connection()
+ destination = destination or sysparam(conn).DEFAULT_PAYMENT_DESTINATION
+ till = Till.get_current(conn)
+ return Payment(due_date=due_date, value=value, till=till,
+ description=description, group=self, method=method,
+ destination=destination, connection=conn)
+
+ def confirm(self, gift_certificate_settings=None):
+ """This can be implemented in a subclass, but it's not required"""
+
+ #
+ # IContainer implementation
+ #
+
+ @argcheck(Payment)
+ def add_item(self, payment):
+ payment.group = self
+
+ @argcheck(Payment)
+ def remove_item(self, payment):
+ Payment.delete(payment.id, connection=self.get_connection())
+
+ def get_items(self):
+ return Payment.selectBy(group=self,
+ connection=self.get_connection())
+
+ #
+ # Fiscal methods
+ #
+
+ def _create_fiscal_entry(self, table, cfop, invoice_number, **kwargs):
+ conn = self.get_connection()
+ drawee = self.get_thirdparty()
+ branch = get_current_branch(conn)
+ return table(connection=conn, invoice_number=invoice_number,
+ cfop=cfop, drawee=drawee, branch=branch,
+ date=datetime.now(), payment_group=self, **kwargs)
+
+ def create_icmsipi_book_entry(self, cfop, invoice_number, icms_value,
+ ipi_value=Decimal(0)):
+ self._create_fiscal_entry(IcmsIpiBookEntry, cfop, invoice_number,
+ icms_value=icms_value, ipi_value=ipi_value)
+
+ def create_iss_book_entry(self, cfop, invoice_number, iss_value):
+ self._create_fiscal_entry(IssBookEntry, cfop, invoice_number,
+ iss_value=iss_value)
+
+ def revert_fiscal_entry(self, invoice_number):
+ conn = self.get_connection()
+ entries = AbstractFiscalBookEntry.selectBy(payment_groupID=self.id,
+ connection=conn)
+ if entries.count() > 1:
+ raise DatabaseInconsistency("You should have only one fiscal "
+ "entry per payment group")
+ if not entries:
+ return
+ entries[0].reverse_entry(invoice_number)
+
+ def _get_paid_payments(self):
+ # FIXME: Logic in SQL
+ statuses = (Payment.STATUS_PAID, Payment.STATUS_REVIEWING,
+ Payment.STATUS_CONFIRMED)
+ return [p for p in self.get_items() if p.status in statuses]
+
+ def _get_unpaid_payments(self):
+ # FIXME: Logic in SQL
+ statuses = Payment.STATUS_PREVIEW, Payment.STATUS_PENDING
+ return [p for p in self.get_items() if p.status in statuses]
+
+ #
+ # Public API
+ #
+
+ def cancel(self, invoice_number):
+ if self.status == AbstractPaymentGroup.STATUS_CANCELLED:
+ raise StoqlibError("This payment group is already cancelled")
+ for payment in self._get_unpaid_payments():
+ payment.cancel()
+ self.status = AbstractPaymentGroup.STATUS_CANCELLED
+ self.cancel_date = datetime.now()
+ self.revert_fiscal_entry(invoice_number)
+
+ @argcheck(Decimal, unicode, object)
+ def create_debit(self, value, description, till):
+ value = - abs(value)
+ return self._create_till_entry(value, description, till)
+
+ @argcheck(Decimal, unicode, object)
+ def create_credit(self, value, description, till):
+ value = abs(value)
+ return self._create_till_entry(value, description, till)
+
+ def get_total_paid(self):
+ # FIXME: Move sum to SQL statement
+ paid_values = [payment.paid_value
+ for payment in self._get_paid_payments()]
+ return sum(paid_values, currency(0))
+
+ def set_method(self, method_iface):
+ items = get_all_methods_dict().items()
+ method = [method_id for method_id, iface in items
+ if method_iface is iface]
+ if len(method) != 1:
+ raise TypeError('Invalid method_class argument, got type %s'
+ % method_iface)
+ self.default_method = method[0]
+
+ def get_till_entries(self):
+ from stoqlib.domain.till import TillEntry
+ return TillEntry.selectBy(payment_groupID=self.id,
+ connection=self.get_connection())
+
+ def setup_inpayments(self):
+ payment_count = self.get_items().count()
+ till_entries_count = self.get_till_entries().count()
+ if not (payment_count or till_entries_count):
+ raise ValueError('You must have at least one payment for each '
+ 'payment group')
+ self.installments_number = payment_count
+
+ # FIXME: Check if all the payments are in STATUS_PREVIEW state?
+ for payment in self.get_items():
+ payment.set_pending()
+
+ def confirm_money_payments(self):
+ from stoqlib.domain.payment.methods import PMAdaptToMoneyPM
+ for payment in self.get_items():
+ if not isinstance(payment.method, PMAdaptToMoneyPM):
+ continue
+ payment.pay()
+
+ def check_close(self):
+ """Verifies if the payment group can be closed and close it.
+
+ @returns: the close status, True if it has been closed or
+ False if not.
+ """
+ if not self.status == AbstractPaymentGroup.STATUS_OPEN:
+ raise ValueError("The status for this payment group should be "
+ "opened, got %s" % self.get_status_string())
+ payments = self.get_items()
+ statuses = [Payment.STATUS_CONFIRMED, Payment.STATUS_CANCELLED]
+ for payment in payments:
+ if payment.status not in statuses:
+ return False
+ self.status = AbstractPaymentGroup.STATUS_CLOSED
+ return True
+
+ def clear_preview_payments(self, ignore_method_iface=None):
+ """Delete payments of preview status associated to the current
+ payment_group. It can happen if user open and cancel this wizard.
+ Notes:
+ ignore_method_iface = a payment method interface which is
+ ignored in the search for payments
+ """
+ query = dict(status=Payment.STATUS_PREVIEW,
+ group=self)
+
+ conn = self.get_connection()
+ if ignore_method_iface:
+ base_method = sysparam(conn).BASE_PAYMENT_METHOD
+ query['method'] = ignore_method_iface(base_method)
+
+ for payment in Payment.selectBy(connection=conn, **query):
+ inpayment = IInPayment(payment, None)
+ if not inpayment:
+ continue
+ payment.method.delete_inpayment(inpayment)
+
+ #
+ # Accessors
+ #
+
+ def get_status_string(self):
+ if not self.status in AbstractPaymentGroup.statuses.keys():
+ raise DatabaseInconsistency("Invalid status, got %d"
+ % self.status)
+ return self.statuses[self.status]
+
+ def get_default_payment_method(self):
+ """This hook must be redefined in a subclass when it's necessary"""
+ return self.default_method
+
+ def get_default_payment_method_name(self):
+ """This hook must be redefined in a subclass when it's necessary"""
+ method_names = get_method_names()
+ if not self.default_method in method_names.keys():
+ raise DatabaseInconsistency('Invalid payment method, got %d'
+ % self.default_method)
+ return method_names[self.default_method]
+
+
+
+#
+# Adapters for Payment class
+#
+
+
+class PaymentAdaptToInPayment(ModelAdapter):
+
+ implements(IInPayment)
+
+ # TODO: Unused
+ def receive(self):
+ payment = self.get_adapted()
+ if not payment.status == Payment.STATUS_PENDING:
+ raise ValueError("This payment is already received.")
+ payment.pay()
+ payment.group.update_thirdparty_status()
+ # TODO we must also add new till entries here
+
+Payment.registerFacet(PaymentAdaptToInPayment, IInPayment)
+
+
+class PaymentAdaptToOutPayment(ModelAdapter):
+
+ implements(IOutPayment)
+
+ def pay(self):
+ payment = self.get_adapted()
+ if not payment.status == Payment.STATUS_PENDING:
+ raise ValueError("This payment is already paid.")
+ payment.pay()
+ # TODO we must also add new till entries here
+
+Payment.registerFacet(PaymentAdaptToOutPayment, IOutPayment)
+
More information about the POS-commit
mailing list