mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-10-31 09:03:06 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			897 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			897 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #
 | |
| # Copyright 2023 © AE UTBM
 | |
| # ae@utbm.fr / ae.info@utbm.fr
 | |
| #
 | |
| # This file is part of the website of the UTBM Student Association (AE UTBM),
 | |
| # https://ae.utbm.fr.
 | |
| #
 | |
| # You can find the source code of the website at https://github.com/ae-utbm/sith
 | |
| #
 | |
| # LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
 | |
| # SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
 | |
| # OR WITHIN THE LOCAL FILE "LICENSE"
 | |
| #
 | |
| #
 | |
| 
 | |
| import collections
 | |
| 
 | |
| from django import forms
 | |
| from django.conf import settings
 | |
| from django.contrib.auth.mixins import PermissionRequiredMixin
 | |
| from django.core.exceptions import PermissionDenied, ValidationError
 | |
| from django.db import transaction
 | |
| from django.db.models import Sum
 | |
| from django.forms import HiddenInput
 | |
| from django.forms.models import modelform_factory
 | |
| from django.http import HttpResponse
 | |
| from django.urls import reverse, reverse_lazy
 | |
| from django.utils.translation import gettext_lazy as _
 | |
| from django.views.generic import DetailView, ListView
 | |
| from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
 | |
| 
 | |
| from accounting.models import (
 | |
|     AccountingType,
 | |
|     BankAccount,
 | |
|     ClubAccount,
 | |
|     Company,
 | |
|     GeneralJournal,
 | |
|     Label,
 | |
|     Operation,
 | |
|     SimplifiedAccountingType,
 | |
| )
 | |
| from accounting.widgets.ajax_select import (
 | |
|     AutoCompleteSelectClubAccount,
 | |
|     AutoCompleteSelectCompany,
 | |
| )
 | |
| from club.models import Club
 | |
| from club.widgets.ajax_select import AutoCompleteSelectClub
 | |
| from core.auth.mixins import (
 | |
|     CanCreateMixin,
 | |
|     CanEditMixin,
 | |
|     CanEditPropMixin,
 | |
|     CanViewMixin,
 | |
| )
 | |
| from core.models import User
 | |
| from core.views.forms import SelectDate, SelectFile
 | |
| from core.views.mixins import TabedViewMixin
 | |
| from core.views.widgets.ajax_select import AutoCompleteSelectUser
 | |
| from counter.models import Counter, Product, Selling
 | |
| 
 | |
| # Main accounting view
 | |
| 
 | |
| 
 | |
| class BankAccountListView(CanViewMixin, ListView):
 | |
|     """A list view for the admins."""
 | |
| 
 | |
|     model = BankAccount
 | |
|     template_name = "accounting/bank_account_list.jinja"
 | |
|     ordering = ["name"]
 | |
| 
 | |
| 
 | |
| # Simplified accounting types
 | |
| 
 | |
| 
 | |
| class SimplifiedAccountingTypeListView(CanViewMixin, ListView):
 | |
|     """A list view for the admins."""
 | |
| 
 | |
|     model = SimplifiedAccountingType
 | |
|     template_name = "accounting/simplifiedaccountingtype_list.jinja"
 | |
| 
 | |
| 
 | |
| class SimplifiedAccountingTypeEditView(CanViewMixin, UpdateView):
 | |
|     """An edit view for the admins."""
 | |
| 
 | |
|     model = SimplifiedAccountingType
 | |
|     pk_url_kwarg = "type_id"
 | |
|     fields = ["label", "accounting_type"]
 | |
|     template_name = "core/edit.jinja"
 | |
| 
 | |
| 
 | |
| class SimplifiedAccountingTypeCreateView(PermissionRequiredMixin, CreateView):
 | |
|     """Create an accounting type (for the admins)."""
 | |
| 
 | |
|     model = SimplifiedAccountingType
 | |
|     fields = ["label", "accounting_type"]
 | |
|     template_name = "core/create.jinja"
 | |
|     permission_required = "accounting.add_simplifiedaccountingtype"
 | |
| 
 | |
| 
 | |
| # Accounting types
 | |
| 
 | |
| 
 | |
| class AccountingTypeListView(CanViewMixin, ListView):
 | |
|     """A list view for the admins."""
 | |
| 
 | |
|     model = AccountingType
 | |
|     template_name = "accounting/accountingtype_list.jinja"
 | |
| 
 | |
| 
 | |
| class AccountingTypeEditView(CanViewMixin, UpdateView):
 | |
|     """An edit view for the admins."""
 | |
| 
 | |
|     model = AccountingType
 | |
|     pk_url_kwarg = "type_id"
 | |
|     fields = ["code", "label", "movement_type"]
 | |
|     template_name = "core/edit.jinja"
 | |
| 
 | |
| 
 | |
| class AccountingTypeCreateView(PermissionRequiredMixin, CreateView):
 | |
|     """Create an accounting type (for the admins)."""
 | |
| 
 | |
|     model = AccountingType
 | |
|     fields = ["code", "label", "movement_type"]
 | |
|     template_name = "core/create.jinja"
 | |
|     permission_required = "accounting.add_accountingtype"
 | |
| 
 | |
| 
 | |
| # BankAccount views
 | |
| 
 | |
| 
 | |
| class BankAccountEditView(CanViewMixin, UpdateView):
 | |
|     """An edit view for the admins."""
 | |
| 
 | |
|     model = BankAccount
 | |
|     pk_url_kwarg = "b_account_id"
 | |
|     fields = ["name", "iban", "number", "club"]
 | |
|     template_name = "core/edit.jinja"
 | |
| 
 | |
| 
 | |
| class BankAccountDetailView(CanViewMixin, DetailView):
 | |
|     """A detail view, listing every club account."""
 | |
| 
 | |
|     model = BankAccount
 | |
|     pk_url_kwarg = "b_account_id"
 | |
|     template_name = "accounting/bank_account_details.jinja"
 | |
| 
 | |
| 
 | |
| class BankAccountCreateView(CanCreateMixin, CreateView):
 | |
|     """Create a bank account (for the admins)."""
 | |
| 
 | |
|     model = BankAccount
 | |
|     fields = ["name", "club", "iban", "number"]
 | |
|     template_name = "core/create.jinja"
 | |
| 
 | |
| 
 | |
| class BankAccountDeleteView(
 | |
|     CanEditPropMixin, DeleteView
 | |
| ):  # TODO change Delete to Close
 | |
|     """Delete a bank account (for the admins)."""
 | |
| 
 | |
|     model = BankAccount
 | |
|     pk_url_kwarg = "b_account_id"
 | |
|     template_name = "core/delete_confirm.jinja"
 | |
|     success_url = reverse_lazy("accounting:bank_list")
 | |
| 
 | |
| 
 | |
| # ClubAccount views
 | |
| 
 | |
| 
 | |
| class ClubAccountEditView(CanViewMixin, UpdateView):
 | |
|     """An edit view for the admins."""
 | |
| 
 | |
|     model = ClubAccount
 | |
|     pk_url_kwarg = "c_account_id"
 | |
|     fields = ["name", "club", "bank_account"]
 | |
|     template_name = "core/edit.jinja"
 | |
| 
 | |
| 
 | |
| class ClubAccountDetailView(CanViewMixin, DetailView):
 | |
|     """A detail view, listing every journal."""
 | |
| 
 | |
|     model = ClubAccount
 | |
|     pk_url_kwarg = "c_account_id"
 | |
|     template_name = "accounting/club_account_details.jinja"
 | |
| 
 | |
| 
 | |
| class ClubAccountCreateView(CanCreateMixin, CreateView):
 | |
|     """Create a club account (for the admins)."""
 | |
| 
 | |
|     model = ClubAccount
 | |
|     fields = ["name", "club", "bank_account"]
 | |
|     template_name = "core/create.jinja"
 | |
| 
 | |
|     def get_initial(self):
 | |
|         ret = super().get_initial()
 | |
|         if "parent" in self.request.GET:
 | |
|             obj = BankAccount.objects.filter(id=int(self.request.GET["parent"])).first()
 | |
|             if obj is not None:
 | |
|                 ret["bank_account"] = obj.id
 | |
|         return ret
 | |
| 
 | |
| 
 | |
| class ClubAccountDeleteView(
 | |
|     CanEditPropMixin, DeleteView
 | |
| ):  # TODO change Delete to Close
 | |
|     """Delete a club account (for the admins)."""
 | |
| 
 | |
|     model = ClubAccount
 | |
|     pk_url_kwarg = "c_account_id"
 | |
|     template_name = "core/delete_confirm.jinja"
 | |
|     success_url = reverse_lazy("accounting:bank_list")
 | |
| 
 | |
| 
 | |
| # Journal views
 | |
| 
 | |
| 
 | |
| class JournalTabsMixin(TabedViewMixin):
 | |
|     def get_tabs_title(self):
 | |
|         return _("Journal")
 | |
| 
 | |
|     def get_list_of_tabs(self):
 | |
|         return [
 | |
|             {
 | |
|                 "url": reverse(
 | |
|                     "accounting:journal_details", kwargs={"j_id": self.object.id}
 | |
|                 ),
 | |
|                 "slug": "journal",
 | |
|                 "name": _("Journal"),
 | |
|             },
 | |
|             {
 | |
|                 "url": reverse(
 | |
|                     "accounting:journal_nature_statement",
 | |
|                     kwargs={"j_id": self.object.id},
 | |
|                 ),
 | |
|                 "slug": "nature_statement",
 | |
|                 "name": _("Statement by nature"),
 | |
|             },
 | |
|             {
 | |
|                 "url": reverse(
 | |
|                     "accounting:journal_person_statement",
 | |
|                     kwargs={"j_id": self.object.id},
 | |
|                 ),
 | |
|                 "slug": "person_statement",
 | |
|                 "name": _("Statement by person"),
 | |
|             },
 | |
|             {
 | |
|                 "url": reverse(
 | |
|                     "accounting:journal_accounting_statement",
 | |
|                     kwargs={"j_id": self.object.id},
 | |
|                 ),
 | |
|                 "slug": "accounting_statement",
 | |
|                 "name": _("Accounting statement"),
 | |
|             },
 | |
|         ]
 | |
| 
 | |
| 
 | |
| class JournalCreateView(CanCreateMixin, CreateView):
 | |
|     """Create a general journal."""
 | |
| 
 | |
|     model = GeneralJournal
 | |
|     form_class = modelform_factory(
 | |
|         GeneralJournal,
 | |
|         fields=["name", "start_date", "club_account"],
 | |
|         widgets={"start_date": SelectDate},
 | |
|     )
 | |
|     template_name = "core/create.jinja"
 | |
| 
 | |
|     def get_initial(self):
 | |
|         ret = super().get_initial()
 | |
|         if "parent" in self.request.GET:
 | |
|             obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first()
 | |
|             if obj is not None:
 | |
|                 ret["club_account"] = obj.id
 | |
|         return ret
 | |
| 
 | |
| 
 | |
| class JournalDetailView(JournalTabsMixin, CanViewMixin, DetailView):
 | |
|     """A detail view, listing every operation."""
 | |
| 
 | |
|     model = GeneralJournal
 | |
|     pk_url_kwarg = "j_id"
 | |
|     template_name = "accounting/journal_details.jinja"
 | |
|     current_tab = "journal"
 | |
| 
 | |
| 
 | |
| class JournalEditView(CanEditMixin, UpdateView):
 | |
|     """Update a general journal."""
 | |
| 
 | |
|     model = GeneralJournal
 | |
|     pk_url_kwarg = "j_id"
 | |
|     fields = ["name", "start_date", "end_date", "club_account", "closed"]
 | |
|     template_name = "core/edit.jinja"
 | |
| 
 | |
| 
 | |
| class JournalDeleteView(CanEditPropMixin, DeleteView):
 | |
|     """Delete a club account (for the admins)."""
 | |
| 
 | |
|     model = GeneralJournal
 | |
|     pk_url_kwarg = "j_id"
 | |
|     template_name = "core/delete_confirm.jinja"
 | |
|     success_url = reverse_lazy("accounting:club_details")
 | |
| 
 | |
|     def dispatch(self, request, *args, **kwargs):
 | |
|         self.object = self.get_object()
 | |
|         if self.object.operations.count() == 0:
 | |
|             return super().dispatch(request, *args, **kwargs)
 | |
|         else:
 | |
|             raise PermissionDenied
 | |
| 
 | |
| 
 | |
| # Operation views
 | |
| 
 | |
| 
 | |
| class OperationForm(forms.ModelForm):
 | |
|     class Meta:
 | |
|         model = Operation
 | |
|         fields = [
 | |
|             "amount",
 | |
|             "remark",
 | |
|             "journal",
 | |
|             "target_type",
 | |
|             "target_id",
 | |
|             "target_label",
 | |
|             "date",
 | |
|             "mode",
 | |
|             "cheque_number",
 | |
|             "invoice",
 | |
|             "simpleaccounting_type",
 | |
|             "accounting_type",
 | |
|             "label",
 | |
|             "done",
 | |
|         ]
 | |
|         widgets = {
 | |
|             "journal": HiddenInput,
 | |
|             "target_id": HiddenInput,
 | |
|             "date": SelectDate,
 | |
|             "invoice": SelectFile,
 | |
|         }
 | |
| 
 | |
|     user = forms.ModelChoiceField(
 | |
|         help_text=None,
 | |
|         required=False,
 | |
|         widget=AutoCompleteSelectUser,
 | |
|         queryset=User.objects.all(),
 | |
|     )
 | |
|     club_account = forms.ModelChoiceField(
 | |
|         help_text=None,
 | |
|         required=False,
 | |
|         widget=AutoCompleteSelectClubAccount,
 | |
|         queryset=ClubAccount.objects.all(),
 | |
|     )
 | |
|     club = forms.ModelChoiceField(
 | |
|         help_text=None,
 | |
|         required=False,
 | |
|         widget=AutoCompleteSelectClub,
 | |
|         queryset=Club.objects.all(),
 | |
|     )
 | |
|     company = forms.ModelChoiceField(
 | |
|         help_text=None,
 | |
|         required=False,
 | |
|         widget=AutoCompleteSelectCompany,
 | |
|         queryset=Company.objects.all(),
 | |
|     )
 | |
|     need_link = forms.BooleanField(
 | |
|         label=_("Link this operation to the target account"),
 | |
|         required=False,
 | |
|         initial=False,
 | |
|     )
 | |
| 
 | |
|     def __init__(self, *args, **kwargs):
 | |
|         club_account = kwargs.pop("club_account", None)
 | |
|         super().__init__(*args, **kwargs)
 | |
|         if club_account:
 | |
|             self.fields["label"].queryset = club_account.labels.order_by("name").all()
 | |
|         if self.instance.target_type == "USER":
 | |
|             self.fields["user"].initial = self.instance.target_id
 | |
|         elif self.instance.target_type == "ACCOUNT":
 | |
|             self.fields["club_account"].initial = self.instance.target_id
 | |
|         elif self.instance.target_type == "CLUB":
 | |
|             self.fields["club"].initial = self.instance.target_id
 | |
|         elif self.instance.target_type == "COMPANY":
 | |
|             self.fields["company"].initial = self.instance.target_id
 | |
| 
 | |
|     def clean(self):
 | |
|         self.cleaned_data = super().clean()
 | |
|         if "target_type" in self.cleaned_data:
 | |
|             if (
 | |
|                 self.cleaned_data.get("user") is None
 | |
|                 and self.cleaned_data.get("club") is None
 | |
|                 and self.cleaned_data.get("club_account") is None
 | |
|                 and self.cleaned_data.get("company") is None
 | |
|                 and self.cleaned_data.get("target_label") == ""
 | |
|             ):
 | |
|                 self.add_error(
 | |
|                     "target_type", ValidationError(_("The target must be set."))
 | |
|                 )
 | |
|             else:
 | |
|                 if self.cleaned_data["target_type"] == "USER":
 | |
|                     self.cleaned_data["target_id"] = self.cleaned_data["user"].id
 | |
|                 elif self.cleaned_data["target_type"] == "ACCOUNT":
 | |
|                     self.cleaned_data["target_id"] = self.cleaned_data[
 | |
|                         "club_account"
 | |
|                     ].id
 | |
|                 elif self.cleaned_data["target_type"] == "CLUB":
 | |
|                     self.cleaned_data["target_id"] = self.cleaned_data["club"].id
 | |
|                 elif self.cleaned_data["target_type"] == "COMPANY":
 | |
|                     self.cleaned_data["target_id"] = self.cleaned_data["company"].id
 | |
| 
 | |
|         if self.cleaned_data.get("amount") is None:
 | |
|             self.add_error("amount", ValidationError(_("The amount must be set.")))
 | |
| 
 | |
|         return self.cleaned_data
 | |
| 
 | |
|     def save(self):
 | |
|         ret = super().save()
 | |
|         if (
 | |
|             self.instance.target_type == "ACCOUNT"
 | |
|             and not self.instance.linked_operation
 | |
|             and self.instance.target.has_open_journal()
 | |
|             and self.cleaned_data["need_link"]
 | |
|         ):
 | |
|             inst = self.instance
 | |
|             club_account = inst.target
 | |
|             acc_type = (
 | |
|                 AccountingType.objects.exclude(movement_type="NEUTRAL")
 | |
|                 .exclude(movement_type=inst.accounting_type.movement_type)
 | |
|                 .order_by("code")
 | |
|                 .first()
 | |
|             )  # Select a random opposite accounting type
 | |
|             op = Operation(
 | |
|                 journal=club_account.get_open_journal(),
 | |
|                 amount=inst.amount,
 | |
|                 date=inst.date,
 | |
|                 remark=inst.remark,
 | |
|                 mode=inst.mode,
 | |
|                 cheque_number=inst.cheque_number,
 | |
|                 invoice=inst.invoice,
 | |
|                 done=False,  # Has to be checked by hand
 | |
|                 simpleaccounting_type=None,
 | |
|                 accounting_type=acc_type,
 | |
|                 target_type="ACCOUNT",
 | |
|                 target_id=inst.journal.club_account.id,
 | |
|                 target_label="",
 | |
|                 linked_operation=inst,
 | |
|             )
 | |
|             op.save()
 | |
|             self.instance.linked_operation = op
 | |
|             self.save()
 | |
|         return ret
 | |
| 
 | |
| 
 | |
| class OperationCreateView(CanCreateMixin, CreateView):
 | |
|     """Create an operation."""
 | |
| 
 | |
|     model = Operation
 | |
|     form_class = OperationForm
 | |
|     template_name = "accounting/operation_edit.jinja"
 | |
| 
 | |
|     def get_form(self, form_class=None):
 | |
|         self.journal = GeneralJournal.objects.filter(id=self.kwargs["j_id"]).first()
 | |
|         ca = self.journal.club_account if self.journal else None
 | |
|         return self.form_class(club_account=ca, **self.get_form_kwargs())
 | |
| 
 | |
|     def get_initial(self):
 | |
|         ret = super().get_initial()
 | |
|         if self.journal is not None:
 | |
|             ret["journal"] = self.journal.id
 | |
|         return ret
 | |
| 
 | |
|     def get_context_data(self, **kwargs):
 | |
|         """Add journal to the context."""
 | |
|         kwargs = super().get_context_data(**kwargs)
 | |
|         if self.journal:
 | |
|             kwargs["object"] = self.journal
 | |
|         return kwargs
 | |
| 
 | |
| 
 | |
| class OperationEditView(CanEditMixin, UpdateView):
 | |
|     """An edit view, working as detail for the moment."""
 | |
| 
 | |
|     model = Operation
 | |
|     pk_url_kwarg = "op_id"
 | |
|     form_class = OperationForm
 | |
|     template_name = "accounting/operation_edit.jinja"
 | |
| 
 | |
|     def get_context_data(self, **kwargs):
 | |
|         """Add journal to the context."""
 | |
|         kwargs = super().get_context_data(**kwargs)
 | |
|         kwargs["object"] = self.object.journal
 | |
|         return kwargs
 | |
| 
 | |
| 
 | |
| class OperationPDFView(CanViewMixin, DetailView):
 | |
|     """Display the PDF of a given operation."""
 | |
| 
 | |
|     model = Operation
 | |
|     pk_url_kwarg = "op_id"
 | |
| 
 | |
|     def get(self, request, *args, **kwargs):
 | |
|         from reportlab.lib import colors
 | |
|         from reportlab.lib.pagesizes import letter
 | |
|         from reportlab.lib.units import cm
 | |
|         from reportlab.lib.utils import ImageReader
 | |
|         from reportlab.pdfbase import pdfmetrics
 | |
|         from reportlab.pdfbase.ttfonts import TTFont
 | |
|         from reportlab.pdfgen import canvas
 | |
|         from reportlab.platypus import Table, TableStyle
 | |
| 
 | |
|         pdfmetrics.registerFont(TTFont("DejaVu", "DejaVuSerif.ttf"))
 | |
| 
 | |
|         self.object = self.get_object()
 | |
|         amount = self.object.amount
 | |
|         remark = self.object.remark
 | |
|         nature = self.object.accounting_type.movement_type
 | |
|         num = self.object.number
 | |
|         date = self.object.date
 | |
|         mode = self.object.mode
 | |
|         club_name = self.object.journal.club_account.name
 | |
|         ti = self.object.journal.name
 | |
|         op_label = self.object.label
 | |
|         club_address = self.object.journal.club_account.club.address
 | |
|         id_op = self.object.id
 | |
| 
 | |
|         if self.object.target_type == "OTHER":
 | |
|             target = self.object.target_label
 | |
|         else:
 | |
|             target = self.object.target.get_display_name()
 | |
| 
 | |
|         response = HttpResponse(content_type="application/pdf")
 | |
|         response["Content-Disposition"] = 'filename="op-%d(%s_on_%s).pdf"' % (
 | |
|             num,
 | |
|             ti,
 | |
|             club_name,
 | |
|         )
 | |
|         p = canvas.Canvas(response)
 | |
| 
 | |
|         p.setFont("DejaVu", 12)
 | |
| 
 | |
|         p.setTitle("%s %d" % (_("Operation"), num))
 | |
|         width, height = letter
 | |
|         im = ImageReader("core/static/core/img/logo.jpg")
 | |
|         iw, ih = im.getSize()
 | |
|         p.drawImage(im, 40, height - 50, width=iw / 2, height=ih / 2)
 | |
| 
 | |
|         labelStr = [["%s %s - %s %s" % (_("Journal"), ti, _("Operation"), num)]]
 | |
| 
 | |
|         label = Table(labelStr, colWidths=[150], rowHeights=[20])
 | |
| 
 | |
|         label.setStyle(TableStyle([("ALIGN", (0, 0), (-1, -1), "RIGHT")]))
 | |
|         w, h = label.wrapOn(label, 0, 0)
 | |
|         label.drawOn(p, width - 180, height)
 | |
| 
 | |
|         p.drawString(
 | |
|             90, height - 100, _("Financial proof: ") + "OP%010d" % (id_op)
 | |
|         )  # Justificatif du libellé
 | |
|         p.drawString(
 | |
|             90, height - 130, _("Club: %(club_name)s") % ({"club_name": club_name})
 | |
|         )
 | |
|         p.drawString(
 | |
|             90,
 | |
|             height - 160,
 | |
|             _("Label: %(op_label)s")
 | |
|             % {"op_label": op_label if op_label is not None else ""},
 | |
|         )
 | |
|         p.drawString(90, height - 190, _("Date: %(date)s") % {"date": date})
 | |
| 
 | |
|         data = []
 | |
| 
 | |
|         data += [
 | |
|             ["%s" % (_("Credit").upper() if nature == "CREDIT" else _("Debit").upper())]
 | |
|         ]
 | |
| 
 | |
|         data += [[_("Amount: %(amount).2f €") % {"amount": amount}]]
 | |
| 
 | |
|         payment_mode = ""
 | |
|         for m in settings.SITH_ACCOUNTING_PAYMENT_METHOD:
 | |
|             if m[0] == mode:
 | |
|                 payment_mode += "[\u00d7]"
 | |
|             else:
 | |
|                 payment_mode += "[  ]"
 | |
|             payment_mode += " %s\n" % (m[1])
 | |
| 
 | |
|         data += [[payment_mode]]
 | |
| 
 | |
|         data += [
 | |
|             [
 | |
|                 "%s : %s"
 | |
|                 % (_("Debtor") if nature == "CREDIT" else _("Creditor"), target),
 | |
|                 "",
 | |
|             ]
 | |
|         ]
 | |
| 
 | |
|         data += [["%s \n%s" % (_("Comment:"), remark)]]
 | |
| 
 | |
|         t = Table(
 | |
|             data, colWidths=[(width - 90 * 2) / 2] * 2, rowHeights=[20, 20, 70, 20, 80]
 | |
|         )
 | |
|         t.setStyle(
 | |
|             TableStyle(
 | |
|                 [
 | |
|                     ("ALIGN", (0, 0), (-1, -1), "CENTER"),
 | |
|                     ("VALIGN", (-2, -1), (-1, -1), "TOP"),
 | |
|                     ("VALIGN", (0, 0), (-1, -2), "MIDDLE"),
 | |
|                     ("INNERGRID", (0, 0), (-1, -1), 0.25, colors.black),
 | |
|                     ("SPAN", (0, 0), (1, 0)),  # line DEBIT/CREDIT
 | |
|                     ("SPAN", (0, 1), (1, 1)),  # line amount
 | |
|                     ("SPAN", (-2, -1), (-1, -1)),  # line comment
 | |
|                     ("SPAN", (0, -2), (-1, -2)),  # line creditor/debtor
 | |
|                     ("SPAN", (0, 2), (1, 2)),  # line payment_mode
 | |
|                     ("ALIGN", (0, 2), (1, 2), "LEFT"),  # line payment_mode
 | |
|                     ("ALIGN", (-2, -1), (-1, -1), "LEFT"),
 | |
|                     ("BOX", (0, 0), (-1, -1), 0.25, colors.black),
 | |
|                 ]
 | |
|             )
 | |
|         )
 | |
| 
 | |
|         signature = []
 | |
|         signature += [[_("Signature:")]]
 | |
| 
 | |
|         tSig = Table(signature, colWidths=[(width - 90 * 2)], rowHeights=[80])
 | |
|         tSig.setStyle(
 | |
|             TableStyle(
 | |
|                 [
 | |
|                     ("VALIGN", (0, 0), (-1, -1), "TOP"),
 | |
|                     ("BOX", (0, 0), (-1, -1), 0.25, colors.black),
 | |
|                 ]
 | |
|             )
 | |
|         )
 | |
| 
 | |
|         w, h = tSig.wrapOn(p, 0, 0)
 | |
|         tSig.drawOn(p, 90, 200)
 | |
| 
 | |
|         w, h = t.wrapOn(p, 0, 0)
 | |
| 
 | |
|         t.drawOn(p, 90, 350)
 | |
| 
 | |
|         p.drawCentredString(10.5 * cm, 2 * cm, club_name)
 | |
|         p.drawCentredString(10.5 * cm, 1 * cm, club_address)
 | |
| 
 | |
|         p.showPage()
 | |
|         p.save()
 | |
|         return response
 | |
| 
 | |
| 
 | |
| class JournalNatureStatementView(JournalTabsMixin, CanViewMixin, DetailView):
 | |
|     """Display a statement sorted by labels."""
 | |
| 
 | |
|     model = GeneralJournal
 | |
|     pk_url_kwarg = "j_id"
 | |
|     template_name = "accounting/journal_statement_nature.jinja"
 | |
|     current_tab = "nature_statement"
 | |
| 
 | |
|     def statement(self, queryset, movement_type):
 | |
|         ret = collections.OrderedDict()
 | |
|         statement = collections.OrderedDict()
 | |
|         total_sum = 0
 | |
|         for sat in [
 | |
|             None,
 | |
|             *list(SimplifiedAccountingType.objects.order_by("label")),
 | |
|         ]:
 | |
|             amount = queryset.filter(
 | |
|                 accounting_type__movement_type=movement_type, simpleaccounting_type=sat
 | |
|             ).aggregate(amount_sum=Sum("amount"))["amount_sum"]
 | |
|             label = sat.label if sat is not None else ""
 | |
|             if amount:
 | |
|                 total_sum += amount
 | |
|                 statement[label] = amount
 | |
|         ret[movement_type] = statement
 | |
|         ret[movement_type + "_sum"] = total_sum
 | |
|         return ret
 | |
| 
 | |
|     def big_statement(self):
 | |
|         label_list = (
 | |
|             self.object.operations.order_by("label").values_list("label").distinct()
 | |
|         )
 | |
|         labels = Label.objects.filter(id__in=label_list).all()
 | |
|         statement = collections.OrderedDict()
 | |
|         gen_statement = collections.OrderedDict()
 | |
|         no_label_statement = collections.OrderedDict()
 | |
|         gen_statement.update(self.statement(self.object.operations.all(), "CREDIT"))
 | |
|         gen_statement.update(self.statement(self.object.operations.all(), "DEBIT"))
 | |
|         statement[_("General statement")] = gen_statement
 | |
|         no_label_statement.update(
 | |
|             self.statement(self.object.operations.filter(label=None).all(), "CREDIT")
 | |
|         )
 | |
|         no_label_statement.update(
 | |
|             self.statement(self.object.operations.filter(label=None).all(), "DEBIT")
 | |
|         )
 | |
|         statement[_("No label operations")] = no_label_statement
 | |
|         for label in labels:
 | |
|             l_stmt = collections.OrderedDict()
 | |
|             journals = self.object.operations.filter(label=label).all()
 | |
|             l_stmt.update(self.statement(journals, "CREDIT"))
 | |
|             l_stmt.update(self.statement(journals, "DEBIT"))
 | |
|             statement[label] = l_stmt
 | |
|         return statement
 | |
| 
 | |
|     def get_context_data(self, **kwargs):
 | |
|         """Add infos to the context."""
 | |
|         kwargs = super().get_context_data(**kwargs)
 | |
|         kwargs["statement"] = self.big_statement()
 | |
|         return kwargs
 | |
| 
 | |
| 
 | |
| class JournalPersonStatementView(JournalTabsMixin, CanViewMixin, DetailView):
 | |
|     """Calculate a dictionary with operation target and sum of operations."""
 | |
| 
 | |
|     model = GeneralJournal
 | |
|     pk_url_kwarg = "j_id"
 | |
|     template_name = "accounting/journal_statement_person.jinja"
 | |
|     current_tab = "person_statement"
 | |
| 
 | |
|     def sum_by_target(self, target_id, target_type, movement_type):
 | |
|         return self.object.operations.filter(
 | |
|             accounting_type__movement_type=movement_type,
 | |
|             target_id=target_id,
 | |
|             target_type=target_type,
 | |
|         ).aggregate(amount_sum=Sum("amount"))["amount_sum"]
 | |
| 
 | |
|     def statement(self, movement_type):
 | |
|         statement = collections.OrderedDict()
 | |
|         for op in (
 | |
|             self.object.operations.filter(accounting_type__movement_type=movement_type)
 | |
|             .order_by("target_type", "target_id")
 | |
|             .distinct()
 | |
|         ):
 | |
|             statement[op.target] = self.sum_by_target(
 | |
|                 op.target_id, op.target_type, movement_type
 | |
|             )
 | |
|         return statement
 | |
| 
 | |
|     def total(self, movement_type):
 | |
|         return sum(self.statement(movement_type).values())
 | |
| 
 | |
|     def get_context_data(self, **kwargs):
 | |
|         """Add journal to the context."""
 | |
|         kwargs = super().get_context_data(**kwargs)
 | |
|         kwargs["credit_statement"] = self.statement("CREDIT")
 | |
|         kwargs["debit_statement"] = self.statement("DEBIT")
 | |
|         kwargs["total_credit"] = self.total("CREDIT")
 | |
|         kwargs["total_debit"] = self.total("DEBIT")
 | |
|         return kwargs
 | |
| 
 | |
| 
 | |
| class JournalAccountingStatementView(JournalTabsMixin, CanViewMixin, DetailView):
 | |
|     """Calculate a dictionary with operation type and sum of operations."""
 | |
| 
 | |
|     model = GeneralJournal
 | |
|     pk_url_kwarg = "j_id"
 | |
|     template_name = "accounting/journal_statement_accounting.jinja"
 | |
|     current_tab = "accounting_statement"
 | |
| 
 | |
|     def statement(self):
 | |
|         statement = collections.OrderedDict()
 | |
|         for at in AccountingType.objects.order_by("code").all():
 | |
|             sum_by_type = self.object.operations.filter(
 | |
|                 accounting_type__code__startswith=at.code
 | |
|             ).aggregate(amount_sum=Sum("amount"))["amount_sum"]
 | |
|             if sum_by_type:
 | |
|                 statement[at] = sum_by_type
 | |
|         return statement
 | |
| 
 | |
|     def get_context_data(self, **kwargs):
 | |
|         """Add journal to the context."""
 | |
|         kwargs = super().get_context_data(**kwargs)
 | |
|         kwargs["statement"] = self.statement()
 | |
|         return kwargs
 | |
| 
 | |
| 
 | |
| # Company views
 | |
| 
 | |
| 
 | |
| class CompanyListView(CanViewMixin, ListView):
 | |
|     model = Company
 | |
|     template_name = "accounting/co_list.jinja"
 | |
| 
 | |
| 
 | |
| class CompanyCreateView(CanCreateMixin, CreateView):
 | |
|     """Create a company."""
 | |
| 
 | |
|     model = Company
 | |
|     fields = ["name"]
 | |
|     template_name = "core/create.jinja"
 | |
|     success_url = reverse_lazy("accounting:co_list")
 | |
| 
 | |
| 
 | |
| class CompanyEditView(CanCreateMixin, UpdateView):
 | |
|     """Edit a company."""
 | |
| 
 | |
|     model = Company
 | |
|     pk_url_kwarg = "co_id"
 | |
|     fields = ["name"]
 | |
|     template_name = "core/edit.jinja"
 | |
|     success_url = reverse_lazy("accounting:co_list")
 | |
| 
 | |
| 
 | |
| # Label views
 | |
| 
 | |
| 
 | |
| class LabelListView(CanViewMixin, DetailView):
 | |
|     model = ClubAccount
 | |
|     pk_url_kwarg = "clubaccount_id"
 | |
|     template_name = "accounting/label_list.jinja"
 | |
| 
 | |
| 
 | |
| class LabelCreateView(
 | |
|     CanCreateMixin, CreateView
 | |
| ):  # FIXME we need to check the rights before creating the object
 | |
|     model = Label
 | |
|     form_class = modelform_factory(
 | |
|         Label, fields=["name", "club_account"], widgets={"club_account": HiddenInput}
 | |
|     )
 | |
|     template_name = "core/create.jinja"
 | |
| 
 | |
|     def get_initial(self):
 | |
|         ret = super().get_initial()
 | |
|         if "parent" in self.request.GET:
 | |
|             obj = ClubAccount.objects.filter(id=int(self.request.GET["parent"])).first()
 | |
|             if obj is not None:
 | |
|                 ret["club_account"] = obj.id
 | |
|         return ret
 | |
| 
 | |
| 
 | |
| class LabelEditView(CanEditMixin, UpdateView):
 | |
|     model = Label
 | |
|     pk_url_kwarg = "label_id"
 | |
|     fields = ["name"]
 | |
|     template_name = "core/edit.jinja"
 | |
| 
 | |
| 
 | |
| class LabelDeleteView(CanEditMixin, DeleteView):
 | |
|     model = Label
 | |
|     pk_url_kwarg = "label_id"
 | |
|     template_name = "core/delete_confirm.jinja"
 | |
| 
 | |
|     def get_success_url(self):
 | |
|         return self.object.get_absolute_url()
 | |
| 
 | |
| 
 | |
| class CloseCustomerAccountForm(forms.Form):
 | |
|     user = forms.ModelChoiceField(
 | |
|         label=_("Refound this account"),
 | |
|         help_text=None,
 | |
|         required=True,
 | |
|         widget=AutoCompleteSelectUser,
 | |
|         queryset=User.objects.all(),
 | |
|     )
 | |
| 
 | |
| 
 | |
| class RefoundAccountView(FormView):
 | |
|     """Create a selling with the same amount than the current user money."""
 | |
| 
 | |
|     template_name = "accounting/refound_account.jinja"
 | |
|     form_class = CloseCustomerAccountForm
 | |
| 
 | |
|     def permission(self, user):
 | |
|         if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
 | |
|             return True
 | |
|         else:
 | |
|             raise PermissionDenied
 | |
| 
 | |
|     def dispatch(self, request, *arg, **kwargs):
 | |
|         res = super().dispatch(request, *arg, **kwargs)
 | |
|         if self.permission(request.user):
 | |
|             return res
 | |
| 
 | |
|     def post(self, request, *arg, **kwargs):
 | |
|         self.operator = request.user
 | |
|         if self.permission(request.user):
 | |
|             return super().post(self, request, *arg, **kwargs)
 | |
| 
 | |
|     def form_valid(self, form):
 | |
|         self.customer = form.cleaned_data["user"]
 | |
|         self.create_selling()
 | |
|         return super().form_valid(form)
 | |
| 
 | |
|     def get_success_url(self):
 | |
|         return reverse("accounting:refound_account")
 | |
| 
 | |
|     def create_selling(self):
 | |
|         with transaction.atomic():
 | |
|             uprice = self.customer.customer.amount
 | |
|             refound_club_counter = Counter.objects.get(
 | |
|                 id=settings.SITH_COUNTER_REFOUND_ID
 | |
|             )
 | |
|             refound_club = refound_club_counter.club
 | |
|             s = Selling(
 | |
|                 label=_("Refound account"),
 | |
|                 unit_price=uprice,
 | |
|                 quantity=1,
 | |
|                 seller=self.operator,
 | |
|                 customer=self.customer.customer,
 | |
|                 club=refound_club,
 | |
|                 counter=refound_club_counter,
 | |
|                 product=Product.objects.get(id=settings.SITH_PRODUCT_REFOUND_ID),
 | |
|             )
 | |
|             s.save()
 |