Source code for stoqlib.gui.editors.paymenteditor

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

##
## Copyright (C) 2007-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, Outc., or visit: http://www.gnu.org/.
##
## Author(s): Stoq Team <stoq-devel@async.com.br>
##
##
""" Editor for payments descriptions and categories"""

import collections

from kiwi import ValueUnset
from kiwi.currency import currency
from kiwi.datatypes import ValidationError
from kiwi.ui.forms import BoolField, ChoiceField, DateField, PriceField, TextField

from stoqlib.api import api
from stoqlib.domain.account import Account
from stoqlib.domain.payment.category import PaymentCategory
from stoqlib.domain.payment.group import PaymentGroup
from stoqlib.domain.payment.method import PaymentMethod
from stoqlib.domain.payment.payment import Payment
from stoqlib.domain.person import Client, Supplier, Branch
from stoqlib.domain.sale import SaleView
from stoqlib.exceptions import SellError
from stoqlib.gui.base.dialogs import run_dialog
from stoqlib.gui.dialogs.stockdecreasedialog import StockDecreaseDetailsDialog
from stoqlib.gui.dialogs.paymentdetails import LonelyPaymentDetailsDialog
from stoqlib.gui.dialogs.purchasedetails import PurchaseDetailsDialog
from stoqlib.gui.dialogs.renegotiationdetails import RenegotiationDetailsDialog
from stoqlib.gui.dialogs.saledetails import SaleDetailsDialog
from stoqlib.gui.editors.baseeditor import BaseEditor
from stoqlib.gui.fields import (AttachmentField, PaymentCategoryField,
                                PersonField, PaymentMethodField,
                                PersonQueryField)
from stoqlib.lib.dateutils import (get_interval_type_items,
                                   interval_type_as_relativedelta,
                                   localtoday)
from stoqlib.lib.decorators import cached_property
from stoqlib.lib.translation import stoqlib_gettext

_ = stoqlib_gettext
_ONCE = -1


class _PaymentEditor(BaseEditor):
    confirm_widgets = ['due_date']
    model_type = Payment
    model_name = _('payment')

    # Override in subclass
    person_type = None
    category_type = None
    payment_type = None
    account_label = None

    @cached_property()
    def fields(self):
        return collections.OrderedDict(
            branch_id=PersonField(_('Branch'), proxy=True, person_type=Branch,
                                  can_add=False, can_edit=False, mandatory=True),
            method=PaymentMethodField(_('Method'),
                                      payment_type=self.payment_type,
                                      proxy=True, mandatory=True, separate=True),
            account=ChoiceField(self.account_label),
            description=TextField(_('Description'), proxy=True, mandatory=True),
            person=PersonQueryField(person_type=self.person_type, proxy=True),
            value=PriceField(_('Value'), proxy=True, mandatory=True),
            due_date=DateField(_('Due date'), proxy=True, mandatory=True),
            category=PaymentCategoryField(_('Category'),
                                          category_type=self.category_type,
                                          proxy=True),
            repeat=ChoiceField(_('Repeat')),
            end_date=DateField(_('End date')),
            attachment=AttachmentField(_('Attachment'))
        )

    def __init__(self, store, model=None, category=None):
        """ A base class for additional payments

        :param store: a store
        :param model: a :class:`stoqlib.domain.payment.payment.Payment` object or None

        """
        BaseEditor.__init__(self, store, model)
        self._setup_widgets()
        if category:
            self.category.select_item_by_label(category)
        self.description.grab_focus()

    #
    # BaseEditor hooks
    #

    def create_model(self, store):
        group = PaymentGroup()
        money = PaymentMethod.get_by_name(store, u'money')
        branch = api.get_current_branch(store)
        # Set status to PENDING now, to avoid calling set_pending on
        # on_confirm for payments that shoud not have its status changed.
        return Payment(open_date=localtoday().date(),
                       branch=branch,
                       status=Payment.STATUS_PENDING,
                       description=u'',
                       value=currency(0),
                       base_value=currency(0),
                       due_date=None,
                       method=money,
                       group=group,
                       category=None,
                       payment_type=self.payment_type,
                       bill_received=False)

    def setup_proxies(self):
        repeat_types = get_interval_type_items(with_multiples=True,
                                               adverb=True)
        repeat_types.insert(0, (_('Once'), _ONCE))
        self.repeat.prefill(repeat_types)
        is_paid = self.model.is_paid()
        # Show account information only after the payment is paid
        if is_paid:
            accounts = Account.get_accounts(self.store)
            self.account.prefill(api.for_combo(accounts, attr='long_description'))
            if self.payment_type == Payment.TYPE_OUT:
                account = self.model.transaction.source_account
            else:
                account = self.model.transaction.account
            self.account.select(account)
            self.account.set_property('sensitive', False)
        else:
            self.account.hide()
            self.account_lbl.hide()
        self.add_proxy(self.model, _PaymentEditor.proxy_widgets)

    def validate_confirm(self):
        if (self.repeat.get_selected() != _ONCE and
                not self._validate_date()):
            return False
        # FIXME: the kiwi view should export it's state and it should
        #        be used by default
        return bool(self.model.description and
                    self.model.due_date and
                    self.model.value)

    def on_confirm(self):
        self.model.base_value = self.model.value
        facet = self.person.read()
        if facet and facet is not ValueUnset:
            setattr(self.model.group,
                    self.person_attribute,
                    facet.person)

        self.model.attachment = self.fields['attachment'].attachment

        self.store.add(self.model.group)
        self.store.add(self.model)

        if self.repeat.get_selected() != _ONCE:
            Payment.create_repeated(self.store, self.model,
                                    self.repeat.get_selected(),
                                    self.model.due_date.date(),
                                    self.end_date.get_date())

    # Private

    def _setup_widgets(self):
        self.person_lbl.set_label(self._person_label)
        lonely_payment = False
        if self.model.group.sale:
            label = _("Sale details")
        elif self.model.group.purchase:
            label = _("Purchase details")
        elif self.model.group._renegotiation:
            label = _("Details")
        else:
            lonely_payment = True
            label = _("Details")
        self.details_button = self.add_button(label)
        self.details_button.connect('clicked',
                                    self._on_details_button__clicked)

        self.end_date.set_sensitive(False)
        if self.edit_mode:
            uneditable_fields = ['value', 'due_date', 'person', 'repeat',
                                 'end_date', 'branch_id', 'method']
            # Let an unpaid lonely payment have the value edited
            if lonely_payment and not self.model.is_paid():
                uneditable_fields.remove('value')

            for field_name in uneditable_fields:
                field = self.fields[field_name]
                field.can_add = False
                field.can_edit = False
                field.set_sensitive(False)

        person = getattr(self.model.group, self.person_attribute)
        if person:
            store = person.store
            facet = store.find(self.person_type, person=person).one()
            self.fields['person'].set_value(facet)

    def _show_order_dialog(self):
        group = self.model.group
        if group.sale:
            sale_view = self.store.find(SaleView, id=group.sale.id).one()
            run_dialog(SaleDetailsDialog, self, self.store, sale_view)
        elif group.purchase:
            run_dialog(PurchaseDetailsDialog, self, self.store, group.purchase)
        elif group._renegotiation:
            run_dialog(RenegotiationDetailsDialog, self, self.store,
                       group._renegotiation)
        elif group.stock_decrease:
            run_dialog(StockDecreaseDetailsDialog, self, self.store,
                       group.stock_decrease)
        else:
            run_dialog(LonelyPaymentDetailsDialog, self, self.store, self.model)

    def _get_min_date_for_interval(self, due_date, interval_type):
        if not due_date or interval_type is None:
            return None
        return due_date + interval_type_as_relativedelta(interval_type)

    def _validate_date(self):
        if not self.end_date.props.sensitive:
            return True
        end_date = self.end_date.get_date()
        due_date = self.due_date.get_date()
        min_date = self._get_min_date_for_interval(due_date, self.repeat.read())

        if end_date and due_date:
            if end_date < due_date:
                self.end_date.set_invalid(_("End date cannot be before start date"))
            elif min_date and end_date < min_date:
                self.end_date.set_invalid(_("End date must be after %s for this "
                                            "repeat interval") %
                                          min_date.strftime('%x'))
            else:
                self.end_date.set_valid()
                self.refresh_ok(self.is_valid)
                return True
        elif not end_date:
            self.end_date.set_invalid(_("Date cannot be empty"))
        elif not due_date:
            self.due_date.set_invalid(_("Date cannot be empty"))
        self.refresh_ok(False)
        return False

    def _validate_person(self):
        payment_type = self.payment_type
        method = self.method.get_selected()
        self.person.set_property('mandatory',
                                 method.operation.require_person(payment_type))

    #
    # Kiwi Callbacks
    #

    def on_value__validate(self, widget, newvalue):
        if newvalue is None or newvalue <= 0:
            return ValidationError(_("The value must be greater than zero."))

    def on_repeat__content_changed(self, repeat):
        if repeat.get_selected() == _ONCE:
            self.end_date.set_sensitive(False)

            # FIXME: need this check so tests won't crash
            if hasattr(self, 'main_dialog'):
                self.refresh_ok(True)

            return

        self.end_date.set_sensitive(True)
        self._validate_date()

    def on_due_date__content_changed(self, due_date):
        self._validate_date()

    def on_end_date__content_changed(self, end_date):
        self._validate_date()

    def _on_details_button__clicked(self, widget):
        self._show_order_dialog()

    def on_method__content_changed(self, method):
        self.person.validate(force=True)
        self._validate_person()


[docs]class InPaymentEditor(_PaymentEditor): payment_type = Payment.TYPE_IN person_attribute = 'payer' person_type = Client _person_label = _("Payer:") account_label = _("Destination account") help_section = 'account-receivable' category_type = PaymentCategory.TYPE_RECEIVABLE
[docs] def on_person__validate(self, widget, value): if not value: return try: # FIXME: model is not being updated correctly value.can_purchase(self.method.read(), self.value.read()) except SellError as e: return ValidationError(e)
[docs] def on_value__changed(self, value): self.person.validate(force=True)
[docs]class OutPaymentEditor(_PaymentEditor): payment_type = Payment.TYPE_OUT person_attribute = 'recipient' person_type = Supplier _person_label = _("Recipient:") account_label = _("Source account") help_section = 'account-payable' category_type = PaymentCategory.TYPE_PAYABLE @cached_property() def fields(self): fields = super(OutPaymentEditor, self).fields fields['bill_received'] = BoolField(_('The bill has arrived.'), proxy=True) return fields
[docs]def get_dialog_for_payment(payment): if payment is None: raise TypeError(payment) if payment.is_inpayment(): return InPaymentEditor if payment.is_outpayment(): return OutPaymentEditor raise TypeError(payment)
[docs]def test(): # pragma nocover creator = api.prepare_test() retval = run_dialog(InPaymentEditor, None, creator.store, None) creator.store.confirm(retval)
if __name__ == '__main__': # pragma nocover test()