# -*- Mode: Python; coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
##
## Copyright (C) 2007-2010 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 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 General Public License for more details.
##
## You should have received a copy of the GNU 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>
##
# pylint: enable=E1101
import datetime
from dateutil.relativedelta import relativedelta
from kiwi.datatypes import converter
from storm.expr import (And, Count, Join, LeftJoin, Or, Sum, Alias,
Select, Cast)
from storm.info import ClassAlias
from stoqlib.database.expr import Date, Field
from stoqlib.database.viewable import Viewable
from stoqlib.domain.account import BankAccount
from stoqlib.domain.payment.card import (CreditProvider,
CreditCardData, CardPaymentDevice)
from stoqlib.domain.payment.category import PaymentCategory
from stoqlib.domain.payment.comment import PaymentComment
from stoqlib.domain.payment.group import PaymentGroup
from stoqlib.domain.payment.method import CheckData, PaymentMethod
from stoqlib.domain.payment.operation import get_payment_operation
from stoqlib.domain.payment.payment import Payment, PaymentChangeHistory
from stoqlib.domain.payment.renegotiation import PaymentRenegotiation
from stoqlib.domain.person import Person, Branch, Company
from stoqlib.domain.purchase import PurchaseOrder
from stoqlib.domain.sale import Sale
from stoqlib.lib.dateutils import localtoday
from stoqlib.lib.parameters import sysparam
from stoqlib.lib.translation import stoqlib_gettext
_ = stoqlib_gettext
_CommentsSummary = Select(columns=[PaymentComment.payment_id,
Alias(Count(PaymentComment.id), 'comments_number')],
tables=[PaymentComment],
group_by=[PaymentComment.payment_id]),
CommentsSummary = Alias(_CommentsSummary, '_comments')
class BasePaymentView(Viewable):
PaymentGroup_Sale = ClassAlias(PaymentGroup, 'payment_group_sale')
PaymentGroup_Purchase = ClassAlias(PaymentGroup, 'payment_group_purchase')
payment = Payment
group = PaymentGroup
purchase = PurchaseOrder
sale = Sale
method = PaymentMethod
branch = Branch
card_data = CreditCardData
check_data = CheckData
# Payment
id = Payment.id
identifier = Payment.identifier
identifier_str = Cast(Payment.identifier, 'text')
description = Payment.description
due_date = Payment.due_date
status = Payment.status
paid_date = Payment.paid_date
value = Payment.value
paid_value = Payment.paid_value
payment_number = Payment.payment_number
group_id = Payment.group_id
branch_id = Payment.branch_id
# PaymentGroup
renegotiated_id = PaymentGroup.renegotiation_id
# PaymentMethod
method_name = PaymentMethod.method_name
method_id = PaymentMethod.id
# PaymentCategory
color = PaymentCategory.color
category = PaymentCategory.name
# PaymentComment
comments_number = Field('_comments', 'comments_number')
# Sale
sale_id = Sale.id
sale_open_date = Sale.open_date
# Purchase
purchase_id = PurchaseOrder.id
_count_tables = [
Payment,
Join(Branch, Payment.branch_id == Branch.id),
LeftJoin(PaymentGroup, PaymentGroup.id == Payment.group_id),
LeftJoin(PaymentCategory, PaymentCategory.id == Payment.category_id),
Join(PaymentMethod, Payment.method_id == PaymentMethod.id),
LeftJoin(CreditCardData, Payment.id == CreditCardData.payment_id),
LeftJoin(CheckData, Payment.id == CheckData.payment_id),
# Purchase
LeftJoin(PaymentGroup_Purchase, PaymentGroup_Purchase.id == Payment.group_id),
LeftJoin(PurchaseOrder, PurchaseOrder.group_id == PaymentGroup_Purchase.id),
# Sale
LeftJoin(PaymentGroup_Sale, PaymentGroup_Sale.id == Payment.group_id),
LeftJoin(Sale, Sale.group_id == PaymentGroup_Sale.id),
]
tables = _count_tables + [
LeftJoin(CommentsSummary, Field('_comments', 'payment_id') == Payment.id),
]
#
# Private
#
def _get_due_date_delta(self):
if self.status in [Payment.STATUS_PAID, Payment.STATUS_CANCELLED]:
return datetime.timedelta(0)
return localtoday().date() - self.due_date.date()
#
# Public API
#
@classmethod
def post_search_callback(cls, sresults):
select = sresults.get_select_expr(Count(1), Sum(cls.value))
return ('count', 'sum'), select
def can_change_due_date(self):
return self.status not in [Payment.STATUS_PAID,
Payment.STATUS_CANCELLED]
def can_cancel_payment(self):
"""Only lonely payments and pending can be cancelled
"""
if self.sale_id or self.purchase_id:
return False
return self.status == Payment.STATUS_PENDING
@property
def method_description(self):
return self.method.description
@property
def status_str(self):
return Payment.statuses[self.status]
def is_late(self):
return self._get_due_date_delta().days > 0
def get_days_late(self):
return max(self._get_due_date_delta().days, 0)
def is_paid(self):
return self.status == Payment.STATUS_PAID
@property
def operation(self):
return self.method.operation
@classmethod
def find_pending(cls, store, due_date=None):
query = cls.status == Payment.STATUS_PENDING
if due_date:
if isinstance(due_date, tuple):
date_query = And(Date(cls.due_date) >= due_date[0],
Date(cls.due_date) <= due_date[1])
else:
date_query = Date(cls.due_date) == due_date
query = And(query, date_query)
return store.find(cls, query)
class InPaymentView(BasePaymentView):
DraweeCompany = ClassAlias(Company, 'drawee_company')
drawee = Person.name
drawee_fancy_name = DraweeCompany.fancy_name
person_id = Person.id
renegotiated_id = PaymentGroup.renegotiation_id
renegotiation_id = PaymentRenegotiation.id
_count_tables = BasePaymentView._count_tables[:]
_count_tables.append(
LeftJoin(Person,
PaymentGroup.payer_id == Person.id))
tables = BasePaymentView.tables[:]
tables.extend([
LeftJoin(Person, PaymentGroup.payer_id == Person.id),
LeftJoin(DraweeCompany, DraweeCompany.person_id == Person.id),
LeftJoin(PaymentRenegotiation,
PaymentRenegotiation.group_id == PaymentGroup.id),
])
clause = (Payment.payment_type == Payment.TYPE_IN)
@property
def renegotiation(self):
if self.renegotiation_id:
return self.store.get(PaymentRenegotiation, self.renegotiation_id)
@property
def renegotiated(self):
if self.renegotiated_id:
return self.store.get(PaymentRenegotiation, self.renegotiated_id)
def get_parent(self):
return self.sale or self.renegotiation
@classmethod
def has_late_payments(cls, store, person):
"""Checks if the provided person has unpaid payments that are overdue
:param person: A :class:`person <stoqlib.domain.person.Person>` to
check if has late payments
:returns: True if the person has overdue payments. False otherwise
"""
tolerance = sysparam.get_int('TOLERANCE_FOR_LATE_PAYMENTS')
query = And(
cls.person_id == person.id,
cls.status == Payment.STATUS_PENDING,
cls.due_date < localtoday() - relativedelta(days=tolerance))
for late_payments in store.find(cls, query):
sale = late_payments.sale
# Exclude payments for external sales as they are handled by an
# external entity (e.g. a payment gateway) meaning that they be
# out of sync with the Stoq database.
if not sale or not sale.is_external():
return True
return False
class OutPaymentView(BasePaymentView):
supplier_name = Person.name
_count_tables = BasePaymentView._count_tables[:]
_count_tables.append(
LeftJoin(Person,
BasePaymentView.PaymentGroup_Sale.recipient_id == Person.id))
tables = BasePaymentView.tables[:]
tables.extend([
LeftJoin(Person,
Person.id == BasePaymentView.PaymentGroup_Sale.recipient_id),
])
clause = (Payment.payment_type == Payment.TYPE_OUT)
[docs]class CardPaymentView(Viewable):
"""A view for credit providers."""
_DraweePerson = ClassAlias(Person, "drawee_person")
payment = Payment
credit_card_data = CreditCardData
#: the branch this payment was created on
branch = Branch
# Payment Columns
id = Payment.id
identifier = Payment.identifier
identifier_str = Cast(Payment.identifier, 'text')
description = Payment.description
due_date = Payment.due_date
paid_date = Payment.paid_date
status = Payment.status
value = Payment.value
# CreditCardData
fare = CreditCardData.fare
fee = CreditCardData.fee
fee_calc = CreditCardData.fee_value
card_type = CreditCardData.card_type
auth = CreditCardData.auth
device_id = CardPaymentDevice.id
device_name = CardPaymentDevice.description
drawee_name = _DraweePerson.name
provider_name = CreditProvider.short_name
sale_id = Sale.id
renegotiation_id = PaymentRenegotiation.id
tables = [
Payment,
Join(PaymentMethod, PaymentMethod.id == Payment.method_id),
Join(CreditCardData, CreditCardData.payment_id == Payment.id),
Join(CreditProvider, CreditProvider.id == CreditCardData.provider_id),
Join(Branch, Branch.id == Payment.branch_id),
LeftJoin(CardPaymentDevice, CardPaymentDevice.id == CreditCardData.device_id),
LeftJoin(PaymentGroup, PaymentGroup.id == Payment.group_id),
LeftJoin(_DraweePerson, _DraweePerson.id == PaymentGroup.payer_id),
LeftJoin(Sale, Sale.group_id == PaymentGroup.id),
LeftJoin(PaymentRenegotiation,
PaymentRenegotiation.group_id == PaymentGroup.id),
]
@property
def status_str(self):
return Payment.statuses[self.status]
@property
def renegotiation(self):
if self.renegotiation_id:
return self.store.get(PaymentRenegotiation, self.renegotiation_id)
class _BillandCheckPaymentView(Viewable):
"""A base view for check and bill payments."""
payment = Payment
#: The branch this paymet was created on
branch = Branch
id = Payment.id
identifier = Payment.identifier
due_date = Payment.due_date
paid_date = Payment.paid_date
status = Payment.status
value = Payment.value
payment_number = Payment.payment_number
method_name = PaymentMethod.method_name
bank_number = BankAccount.bank_number
bank_branch = BankAccount.bank_branch
bank_account = BankAccount.bank_account
tables = [
Payment,
Join(Branch, Payment.branch_id == Branch.id),
LeftJoin(CheckData, Payment.id == CheckData.payment_id),
Join(PaymentMethod, Payment.method_id == PaymentMethod.id),
LeftJoin(BankAccount, BankAccount.id == CheckData.bank_account_id),
]
clause = Or(PaymentMethod.method_name == u'bill',
PaymentMethod.method_name == u'check')
@property
def status_str(self):
return Payment.statuses[self.status]
@property
def method_description(self):
return get_payment_operation(self.method_name).description
[docs]class InCheckPaymentView(_BillandCheckPaymentView):
"""Stores information about bill and check receivings.
"""
clause = And(_BillandCheckPaymentView.clause,
Payment.payment_type == Payment.TYPE_IN)
[docs]class OutCheckPaymentView(_BillandCheckPaymentView):
"""Stores information about bill and check payments.
"""
bill_received = Payment.bill_received
clause = And(_BillandCheckPaymentView.clause,
Payment.payment_type == Payment.TYPE_OUT)
[docs]class PaymentChangeHistoryView(Viewable):
"""Holds information about changes to a payment.
"""
id = PaymentChangeHistory.id
description = Payment.description
reason = PaymentChangeHistory.change_reason
change_date = PaymentChangeHistory.change_date
last_due_date = PaymentChangeHistory.last_due_date
new_due_date = PaymentChangeHistory.new_due_date
last_status = PaymentChangeHistory.last_status
new_status = PaymentChangeHistory.new_status
tables = [
PaymentChangeHistory,
Join(Payment, Payment.id == PaymentChangeHistory.payment_id)
]
@classmethod
def find_by_group(cls, store, group):
return store.find(cls, Payment.group_id == group.id)
@property
def changed_field(self):
"""Return the name of the changed field."""
if self.last_due_date:
return _('Due Date')
elif self.last_status:
return _('Status')
@property
def from_value(self):
if self.last_due_date:
return converter.as_string(datetime.date, self.last_due_date)
elif self.last_status:
return Payment.statuses[self.last_status]
@property
def to_value(self):
if self.new_due_date:
return converter.as_string(datetime.date, self.new_due_date)
elif self.new_status:
return Payment.statuses[self.new_status]