forms.py 10.2 KB
Newer Older
1 2 3 4 5 6 7
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017  Gabriel Détraz
# Copyright © 2017  Goulven Kermarec
# Copyright © 2017  Augustin Lemesle
8
# Copyright © 2018  Hugo Levy-Falk
9 10 11 12 13 14 15 16 17 18 19 20 21 22
#
# 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.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23
"""
24
Forms for the 'cotisation' app of re2o. It highly depends on
25 26 27 28 29 30 31 32 33 34 35
:cotisations:models and is mainly used by :cotisations:views.

The following forms are mainly used to create, edit or delete
anything related to 'cotisations' :
    * Payments Methods
    * Banks
    * Invoices
    * Articles

See the details for each of these operations in the documentation
of each of the method.
36
"""
37 38
from __future__ import unicode_literals

39
from django import forms
40
from django.db.models import Q
Dalahro's avatar
Dalahro committed
41
from django.forms import ModelForm, Form
42
from django.core.validators import MinValueValidator
43 44

from django.utils.translation import ugettext_lazy as _
45
from django.shortcuts import get_object_or_404
46

47
from re2o.field_permissions import FieldPermissionFormMixin
48
from re2o.mixins import FormRevMixin
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
49 50
from .models import (
    Article, Paiement, Facture, Banque,
51
    CustomInvoice, Vente, CostEstimate,
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
52
)
53
from .payment_methods import balance
54

55

56
class FactureForm(FieldPermissionFormMixin, FormRevMixin, ModelForm):
57
    """
58
    Form used to manage and create an invoice and its fields.
59
    """
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
60

61 62
    def __init__(self, *args, creation=False, **kwargs):
        user = kwargs['user']
63
        prefix = kwargs.pop('prefix', self.Meta.model.__name__)
64
        super(FactureForm, self).__init__(*args, prefix=prefix, **kwargs)
65 66
        self.fields['paiement'].empty_label = \
            _("Select a payment method")
67
        self.fields['paiement'].queryset = Paiement.find_allowed_payments(user)
68 69 70 71 72 73 74
        if not creation:
            self.fields['user'].label = _("Member")
            self.fields['user'].empty_label = \
                _("Select the proprietary member")
            self.fields['valid'].label = _("Validated invoice")
        else:
            self.fields = {'paiement': self.fields['paiement']}
75

76 77
    class Meta:
        model = Facture
78
        fields = '__all__'
79 80

    def clean(self):
81
        cleaned_data = super(FactureForm, self).clean()
82
        paiement = cleaned_data.get('paiement')
83
        if not paiement:
84
            raise forms.ValidationError(
85
                _("A payment method must be specified.")
86
            )
87 88
        return cleaned_data

chirac's avatar
chirac committed
89

90
class SelectArticleForm(FormRevMixin, Form):
91
    """
92 93
    Form used to select an article during the creation of an invoice for a
    member.
94
    """
chirac's avatar
chirac committed
95
    article = forms.ModelChoiceField(
96
        queryset=Article.objects.none(),
97
        label=_("Article"),
98 99 100
        required=True
    )
    quantity = forms.IntegerField(
101
        label=_("Quantity"),
102 103 104 105
        validators=[MinValueValidator(1)],
        required=True
    )

106
    def __init__(self, *args, **kwargs):
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
107
        user = kwargs.pop('user')
108
        target_user = kwargs.pop('target_user', None)
109
        super(SelectArticleForm, self).__init__(*args, **kwargs)
110 111 112 113 114 115 116 117 118
        self.fields['article'].queryset = Article.find_allowed_articles(
            user, target_user)


class DiscountForm(Form):
    """
    Form used in oder to create a discount on an invoice.
    """
    is_relative = forms.BooleanField(
119
        label=_("Discount is on percentage."),
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
        required=False,
    )
    discount = forms.DecimalField(
        label=_("Discount"),
        max_value=100,
        min_value=0,
        max_digits=5,
        decimal_places=2,
        required=False,
    )

    def apply_to_invoice(self, invoice):
        invoice_price = invoice.prix_total()
        discount = self.cleaned_data['discount']
        is_relative = self.cleaned_data['is_relative']
        if is_relative:
            amount = discount/100 * invoice_price
        else:
            amount = discount
139
        if amount:
140 141 142 143 144 145 146 147
            name = _("{}% discount") if is_relative else _("{}€ discount")
            name = name.format(discount)
            Vente.objects.create(
                facture=invoice,
                name=name,
                prix=-amount,
                number=1
            )
148

149

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
150
class CustomInvoiceForm(FormRevMixin, ModelForm):
151
    """
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
152
    Form used to create a custom invoice.
153
    """
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
154 155 156
    class Meta:
        model = CustomInvoice
        fields = '__all__'
157

chirac's avatar
chirac committed
158

Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
159 160 161 162 163 164 165 166 167
class CostEstimateForm(FormRevMixin, ModelForm):
    """
    Form used to create a cost estimate.
    """
    class Meta:
        model = CostEstimate
        exclude = ['paid', 'final_invoice']


168
class ArticleForm(FormRevMixin, ModelForm):
169 170 171
    """
    Form used to create an article.
    """
172 173 174 175 176
    class Meta:
        model = Article
        fields = '__all__'

    def __init__(self, *args, **kwargs):
177
        prefix = kwargs.pop('prefix', self.Meta.model.__name__)
178
        super(ArticleForm, self).__init__(*args, prefix=prefix, **kwargs)
179
        self.fields['name'].label = _("Article name")
180

chirac's avatar
chirac committed
181

182
class DelArticleForm(FormRevMixin, Form):
183 184 185 186
    """
    Form used to delete one or more of the currently available articles.
    The user must choose the one to delete by checking the boxes.
    """
chirac's avatar
chirac committed
187
    articles = forms.ModelMultipleChoiceField(
188
        queryset=Article.objects.none(),
189
        label=_("Available articles"),
chirac's avatar
chirac committed
190 191 192
        widget=forms.CheckboxSelectMultiple
    )

193 194 195 196 197 198 199 200
    def __init__(self, *args, **kwargs):
        instances = kwargs.pop('instances', None)
        super(DelArticleForm, self).__init__(*args, **kwargs)
        if instances:
            self.fields['articles'].queryset = instances
        else:
            self.fields['articles'].queryset = Article.objects.all()

201

202
# TODO : change Paiement to Payment
203
class PaiementForm(FormRevMixin, ModelForm):
204 205 206 207 208
    """
    Form used to create a new payment method.
    The 'cheque' type is used to associate a specific behaviour requiring
    a cheque number and a bank.
    """
209 210
    class Meta:
        model = Paiement
211
        # TODO : change moyen to method and type_paiement to payment_type
Hugo LEVY-FALK's avatar
Hugo LEVY-FALK committed
212
        fields = ['moyen', 'available_for_everyone']
213 214

    def __init__(self, *args, **kwargs):
215
        prefix = kwargs.pop('prefix', self.Meta.model.__name__)
216
        super(PaiementForm, self).__init__(*args, prefix=prefix, **kwargs)
217
        self.fields['moyen'].label = _("Payment method name")
218

chirac's avatar
chirac committed
219

220
# TODO : change paiement to payment
221
class DelPaiementForm(FormRevMixin, Form):
222 223 224 225
    """
    Form used to delete one or more payment methods.
    The user must choose the one to delete by checking the boxes.
    """
226
    # TODO : change paiement to payment
chirac's avatar
chirac committed
227
    paiements = forms.ModelMultipleChoiceField(
228
        queryset=Paiement.objects.none(),
229
        label=_("Available payment methods"),
chirac's avatar
chirac committed
230 231 232
        widget=forms.CheckboxSelectMultiple
    )

233 234 235 236 237 238 239 240
    def __init__(self, *args, **kwargs):
        instances = kwargs.pop('instances', None)
        super(DelPaiementForm, self).__init__(*args, **kwargs)
        if instances:
            self.fields['paiements'].queryset = instances
        else:
            self.fields['paiements'].queryset = Paiement.objects.all()

241

242
# TODO : change banque to bank
243
class BanqueForm(FormRevMixin, ModelForm):
244 245 246
    """
    Form used to create a bank.
    """
chirac's avatar
chirac committed
247
    class Meta:
248
        # TODO : change banque to bank
chirac's avatar
chirac committed
249 250 251 252
        model = Banque
        fields = ['name']

    def __init__(self, *args, **kwargs):
253
        prefix = kwargs.pop('prefix', self.Meta.model.__name__)
254
        super(BanqueForm, self).__init__(*args, prefix=prefix, **kwargs)
255
        self.fields['name'].label = _("Bank name")
chirac's avatar
chirac committed
256

chirac's avatar
chirac committed
257

258
# TODO : change banque to bank
259
class DelBanqueForm(FormRevMixin, Form):
260 261 262 263
    """
    Form used to delete one or more banks.
    The use must choose the one to delete by checking the boxes.
    """
264
    # TODO : change banque to bank
chirac's avatar
chirac committed
265
    banques = forms.ModelMultipleChoiceField(
266
        queryset=Banque.objects.none(),
267
        label=_("Available banks"),
chirac's avatar
chirac committed
268 269
        widget=forms.CheckboxSelectMultiple
    )
270 271 272 273 274 275 276 277

    def __init__(self, *args, **kwargs):
        instances = kwargs.pop('instances', None)
        super(DelBanqueForm, self).__init__(*args, **kwargs)
        if instances:
            self.fields['banques'].queryset = instances
        else:
            self.fields['banques'].queryset = Banque.objects.all()
278 279


280
# TODO : Better name and docstring
281
class RechargeForm(FormRevMixin, Form):
282 283 284
    """
    Form used to refill a user's balance
    """
285
    value = forms.DecimalField(
286
        label=_("Amount"),
287
        min_value=0.01,
288
        validators=[]
289
    )
290 291
    payment = forms.ModelChoiceField(
        queryset=Paiement.objects.none(),
292
        label=_("Payment method")
293
    )
294

295
    def __init__(self, *args, user=None, user_source=None, **kwargs):
296
        self.user = user
297
        super(RechargeForm, self).__init__(*args, **kwargs)
298 299
        self.fields['payment'].empty_label = \
            _("Select a payment method")
300 301
        self.fields['payment'].queryset = Paiement.find_allowed_payments(
            user_source).exclude(is_balance=True)
302

303
    def clean(self):
304
        """
305
        Returns a cleaned value from the received form by validating
306 307
        the value is well inside the possible limits
        """
308
        value = self.cleaned_data['value']
Maël Kervella's avatar
Maël Kervella committed
309
        balance_method = get_object_or_404(balance.PaymentMethod)
310 311
        if balance_method.maximum_balance is not None and \
           value + self.user.solde > balance_method.maximum_balance:
312
            raise forms.ValidationError(
313 314
                _("Requested amount is too high. Your balance can't exceed"
                  " %(max_online_balance)s €.") % {
315
                    'max_online_balance': balance_method.maximum_balance
316 317
                }
            )
318
        return self.cleaned_data