Merge 0e66bf5b5c7714a63ea265f0265b9352994fc338 into bb3dfb7e8a87e4c4ca61d2ee095bb6c3f7ffc115

This commit is contained in:
thomas girod 2025-03-14 17:11:18 +00:00 committed by GitHub
commit 285b6503a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 456 additions and 3607 deletions

View File

@ -1,36 +0,0 @@
#
# 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"
#
#
from django.contrib import admin
from accounting.models import (
AccountingType,
BankAccount,
ClubAccount,
Company,
GeneralJournal,
Label,
Operation,
SimplifiedAccountingType,
)
admin.site.register(BankAccount)
admin.site.register(ClubAccount)
admin.site.register(GeneralJournal)
admin.site.register(AccountingType)
admin.site.register(SimplifiedAccountingType)
admin.site.register(Operation)
admin.site.register(Label)
admin.site.register(Company)

View File

@ -1,23 +0,0 @@
from typing import Annotated
from annotated_types import MinLen
from ninja_extra import ControllerBase, api_controller, paginate, route
from ninja_extra.pagination import PageNumberPaginationExtra
from ninja_extra.schemas import PaginatedResponseSchema
from accounting.models import ClubAccount, Company
from accounting.schemas import ClubAccountSchema, CompanySchema
from core.auth.api_permissions import CanAccessLookup
@api_controller("/lookup", permissions=[CanAccessLookup])
class AccountingController(ControllerBase):
@route.get("/club-account", response=PaginatedResponseSchema[ClubAccountSchema])
@paginate(PageNumberPaginationExtra, page_size=50)
def search_club_account(self, search: Annotated[str, MinLen(1)]):
return ClubAccount.objects.filter(name__icontains=search).values()
@route.get("/company", response=PaginatedResponseSchema[CompanySchema])
@paginate(PageNumberPaginationExtra, page_size=50)
def search_company(self, search: Annotated[str, MinLen(1)]):
return Company.objects.filter(name__icontains=search).values()

View File

@ -4,7 +4,7 @@ import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
import accounting.models
import counter.fields
class Migration(migrations.Migration):
@ -142,7 +142,7 @@ class Migration(migrations.Migration):
),
(
"amount",
accounting.models.CurrencyField(
counter.fields.CurrencyField(
decimal_places=2,
default=0,
verbose_name="amount",
@ -151,7 +151,7 @@ class Migration(migrations.Migration):
),
(
"effective_amount",
accounting.models.CurrencyField(
counter.fields.CurrencyField(
decimal_places=2,
default=0,
verbose_name="effective_amount",
@ -176,7 +176,7 @@ class Migration(migrations.Migration):
("number", models.IntegerField(verbose_name="number")),
(
"amount",
accounting.models.CurrencyField(
counter.fields.CurrencyField(
decimal_places=2, max_digits=12, verbose_name="amount"
),
),

View File

@ -0,0 +1,34 @@
# Generated by Django 4.2.20 on 2025-03-14 16:06
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("accounting", "0005_auto_20170324_0917")]
operations = [
migrations.RemoveField(model_name="bankaccount", name="club"),
migrations.RemoveField(model_name="clubaccount", name="bank_account"),
migrations.RemoveField(model_name="clubaccount", name="club"),
migrations.DeleteModel(name="Company"),
migrations.RemoveField(model_name="generaljournal", name="club_account"),
migrations.AlterUniqueTogether(name="label", unique_together=None),
migrations.RemoveField(model_name="label", name="club_account"),
migrations.AlterUniqueTogether(name="operation", unique_together=None),
migrations.RemoveField(model_name="operation", name="accounting_type"),
migrations.RemoveField(model_name="operation", name="invoice"),
migrations.RemoveField(model_name="operation", name="journal"),
migrations.RemoveField(model_name="operation", name="label"),
migrations.RemoveField(model_name="operation", name="linked_operation"),
migrations.RemoveField(model_name="operation", name="simpleaccounting_type"),
migrations.RemoveField(
model_name="simplifiedaccountingtype", name="accounting_type"
),
migrations.DeleteModel(name="AccountingType"),
migrations.DeleteModel(name="BankAccount"),
migrations.DeleteModel(name="ClubAccount"),
migrations.DeleteModel(name="GeneralJournal"),
migrations.DeleteModel(name="Label"),
migrations.DeleteModel(name="Operation"),
migrations.DeleteModel(name="SimplifiedAccountingType"),
]

View File

@ -12,509 +12,3 @@
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
from decimal import Decimal
from django.conf import settings
from django.core import validators
from django.core.exceptions import ValidationError
from django.db import models
from django.template import defaultfilters
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from club.models import Club
from core.models import SithFile, User
class CurrencyField(models.DecimalField):
"""Custom database field used for currency."""
def __init__(self, *args, **kwargs):
kwargs["max_digits"] = 12
kwargs["decimal_places"] = 2
super().__init__(*args, **kwargs)
def to_python(self, value):
try:
return super().to_python(value).quantize(Decimal("0.01"))
except AttributeError:
return None
if settings.TESTING:
from model_bakery import baker
baker.generators.add(
CurrencyField,
lambda: baker.random_gen.gen_decimal(max_digits=8, decimal_places=2),
)
else: # pragma: no cover
# baker is only used in tests, so we don't need coverage for this part
pass
# Accounting classes
class Company(models.Model):
name = models.CharField(_("name"), max_length=60)
street = models.CharField(_("street"), max_length=60, blank=True)
city = models.CharField(_("city"), max_length=60, blank=True)
postcode = models.CharField(_("postcode"), max_length=10, blank=True)
country = models.CharField(_("country"), max_length=32, blank=True)
phone = PhoneNumberField(_("phone"), blank=True)
email = models.EmailField(_("email"), blank=True)
website = models.CharField(_("website"), max_length=64, blank=True)
class Meta:
verbose_name = _("company")
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("accounting:co_edit", kwargs={"co_id": self.id})
def get_display_name(self):
return self.name
def is_owned_by(self, user):
"""Check if that object can be edited by the given user."""
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
def can_be_edited_by(self, user):
"""Check if that object can be edited by the given user."""
return user.memberships.filter(
end_date=None, club__role=settings.SITH_CLUB_ROLES_ID["Treasurer"]
).exists()
def can_be_viewed_by(self, user):
"""Check if that object can be viewed by the given user."""
return user.memberships.filter(
end_date=None, club__role_gte=settings.SITH_CLUB_ROLES_ID["Treasurer"]
).exists()
class BankAccount(models.Model):
name = models.CharField(_("name"), max_length=30)
iban = models.CharField(_("iban"), max_length=255, blank=True)
number = models.CharField(_("account number"), max_length=255, blank=True)
club = models.ForeignKey(
Club,
related_name="bank_accounts",
verbose_name=_("club"),
on_delete=models.CASCADE,
)
class Meta:
verbose_name = _("Bank account")
ordering = ["club", "name"]
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("accounting:bank_details", kwargs={"b_account_id": self.id})
def is_owned_by(self, user):
"""Check if that object can be edited by the given user."""
if user.is_anonymous:
return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
m = self.club.get_membership_for(user)
return m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
class ClubAccount(models.Model):
name = models.CharField(_("name"), max_length=30)
club = models.ForeignKey(
Club,
related_name="club_account",
verbose_name=_("club"),
on_delete=models.CASCADE,
)
bank_account = models.ForeignKey(
BankAccount,
related_name="club_accounts",
verbose_name=_("bank account"),
on_delete=models.CASCADE,
)
class Meta:
verbose_name = _("Club account")
ordering = ["bank_account", "name"]
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("accounting:club_details", kwargs={"c_account_id": self.id})
def is_owned_by(self, user):
"""Check if that object can be edited by the given user."""
if user.is_anonymous:
return False
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
def can_be_edited_by(self, user):
"""Check if that object can be edited by the given user."""
m = self.club.get_membership_for(user)
return m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]
def can_be_viewed_by(self, user):
"""Check if that object can be viewed by the given user."""
m = self.club.get_membership_for(user)
return m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
def has_open_journal(self):
return self.journals.filter(closed=False).exists()
def get_open_journal(self):
return self.journals.filter(closed=False).first()
def get_display_name(self):
return _("%(club_account)s on %(bank_account)s") % {
"club_account": self.name,
"bank_account": self.bank_account,
}
class GeneralJournal(models.Model):
"""Class storing all the operations for a period of time."""
start_date = models.DateField(_("start date"))
end_date = models.DateField(_("end date"), null=True, blank=True, default=None)
name = models.CharField(_("name"), max_length=40)
closed = models.BooleanField(_("is closed"), default=False)
club_account = models.ForeignKey(
ClubAccount,
related_name="journals",
null=False,
verbose_name=_("club account"),
on_delete=models.CASCADE,
)
amount = CurrencyField(_("amount"), default=0)
effective_amount = CurrencyField(_("effective_amount"), default=0)
class Meta:
verbose_name = _("General journal")
ordering = ["-start_date"]
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("accounting:journal_details", kwargs={"j_id": self.id})
def is_owned_by(self, user):
"""Check if that object can be edited by the given user."""
if user.is_anonymous:
return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
return self.club_account.can_be_edited_by(user)
def can_be_edited_by(self, user):
"""Check if that object can be edited by the given user."""
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
return self.club_account.can_be_edited_by(user)
def can_be_viewed_by(self, user):
return self.club_account.can_be_viewed_by(user)
def update_amounts(self):
self.amount = 0
self.effective_amount = 0
for o in self.operations.all():
if o.accounting_type.movement_type == "CREDIT":
if o.done:
self.effective_amount += o.amount
self.amount += o.amount
else:
if o.done:
self.effective_amount -= o.amount
self.amount -= o.amount
self.save()
class Operation(models.Model):
"""An operation is a line in the journal, a debit or a credit."""
number = models.IntegerField(_("number"))
journal = models.ForeignKey(
GeneralJournal,
related_name="operations",
null=False,
verbose_name=_("journal"),
on_delete=models.CASCADE,
)
amount = CurrencyField(_("amount"))
date = models.DateField(_("date"))
remark = models.CharField(_("comment"), max_length=128, null=True, blank=True)
mode = models.CharField(
_("payment method"),
max_length=255,
choices=settings.SITH_ACCOUNTING_PAYMENT_METHOD,
)
cheque_number = models.CharField(
_("cheque number"), max_length=32, default="", null=True, blank=True
)
invoice = models.ForeignKey(
SithFile,
related_name="operations",
verbose_name=_("invoice"),
null=True,
blank=True,
on_delete=models.CASCADE,
)
done = models.BooleanField(_("is done"), default=False)
simpleaccounting_type = models.ForeignKey(
"SimplifiedAccountingType",
related_name="operations",
verbose_name=_("simple type"),
null=True,
blank=True,
on_delete=models.CASCADE,
)
accounting_type = models.ForeignKey(
"AccountingType",
related_name="operations",
verbose_name=_("accounting type"),
null=True,
blank=True,
on_delete=models.CASCADE,
)
label = models.ForeignKey(
"Label",
related_name="operations",
verbose_name=_("label"),
null=True,
blank=True,
on_delete=models.SET_NULL,
)
target_type = models.CharField(
_("target type"),
max_length=10,
choices=[
("USER", _("User")),
("CLUB", _("Club")),
("ACCOUNT", _("Account")),
("COMPANY", _("Company")),
("OTHER", _("Other")),
],
)
target_id = models.IntegerField(_("target id"), null=True, blank=True)
target_label = models.CharField(
_("target label"), max_length=32, default="", blank=True
)
linked_operation = models.OneToOneField(
"self",
related_name="operation_linked_to",
verbose_name=_("linked operation"),
null=True,
blank=True,
default=None,
on_delete=models.CASCADE,
)
class Meta:
unique_together = ("number", "journal")
ordering = ["-number"]
def __str__(self):
return f"{self.amount} € | {self.date} | {self.accounting_type} | {self.done}"
def __getattribute__(self, attr):
if attr == "target":
return self.get_target()
else:
return object.__getattribute__(self, attr)
def save(self, *args, **kwargs):
if self.number is None:
self.number = self.journal.operations.count() + 1
super().save(*args, **kwargs)
self.journal.update_amounts()
def get_absolute_url(self):
return reverse("accounting:journal_details", kwargs={"j_id": self.journal.id})
def clean(self):
super().clean()
if self.date is None:
raise ValidationError(_("The date must be set."))
elif self.date < self.journal.start_date:
raise ValidationError(
_(
"""The date can not be before the start date of the journal, which is
%(start_date)s."""
)
% {
"start_date": defaultfilters.date(
self.journal.start_date, settings.DATE_FORMAT
)
}
)
if self.target_type != "OTHER" and self.get_target() is None:
raise ValidationError(_("Target does not exists"))
if self.target_type == "OTHER" and self.target_label == "":
raise ValidationError(
_("Please add a target label if you set no existing target")
)
if not self.accounting_type and not self.simpleaccounting_type:
raise ValidationError(
_(
"You need to provide ether a simplified accounting type or a standard accounting type"
)
)
if self.simpleaccounting_type:
self.accounting_type = self.simpleaccounting_type.accounting_type
@property
def target(self):
return self.get_target()
def get_target(self):
tar = None
if self.target_type == "USER":
tar = User.objects.filter(id=self.target_id).first()
elif self.target_type == "CLUB":
tar = Club.objects.filter(id=self.target_id).first()
elif self.target_type == "ACCOUNT":
tar = ClubAccount.objects.filter(id=self.target_id).first()
elif self.target_type == "COMPANY":
tar = Company.objects.filter(id=self.target_id).first()
return tar
def is_owned_by(self, user):
"""Check if that object can be edited by the given user."""
if user.is_anonymous:
return False
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
if self.journal.closed:
return False
m = self.journal.club_account.club.get_membership_for(user)
return m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]
def can_be_edited_by(self, user):
"""Check if that object can be edited by the given user."""
if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID):
return True
if self.journal.closed:
return False
m = self.journal.club_account.club.get_membership_for(user)
return m is not None and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]
class AccountingType(models.Model):
"""Accounting types.
Those are numbers used in accounting to classify operations
"""
code = models.CharField(
_("code"),
max_length=16,
validators=[
validators.RegexValidator(
r"^[0-9]*$", _("An accounting type code contains only numbers")
)
],
)
label = models.CharField(_("label"), max_length=128)
movement_type = models.CharField(
_("movement type"),
choices=[
("CREDIT", _("Credit")),
("DEBIT", _("Debit")),
("NEUTRAL", _("Neutral")),
],
max_length=12,
)
class Meta:
verbose_name = _("accounting type")
ordering = ["movement_type", "code"]
def __str__(self):
return self.code + " - " + self.get_movement_type_display() + " - " + self.label
def get_absolute_url(self):
return reverse("accounting:type_list")
def is_owned_by(self, user):
"""Check if that object can be edited by the given user."""
if user.is_anonymous:
return False
return user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
class SimplifiedAccountingType(models.Model):
"""Simplified version of `AccountingType`."""
label = models.CharField(_("label"), max_length=128)
accounting_type = models.ForeignKey(
AccountingType,
related_name="simplified_types",
verbose_name=_("simplified accounting types"),
on_delete=models.CASCADE,
)
class Meta:
verbose_name = _("simplified type")
ordering = ["accounting_type__movement_type", "accounting_type__code"]
def __str__(self):
return (
f"{self.get_movement_type_display()} "
f"- {self.accounting_type.code} - {self.label}"
)
def get_absolute_url(self):
return reverse("accounting:simple_type_list")
@property
def movement_type(self):
return self.accounting_type.movement_type
def get_movement_type_display(self):
return self.accounting_type.get_movement_type_display()
class Label(models.Model):
"""Label allow a club to sort its operations."""
name = models.CharField(_("label"), max_length=64)
club_account = models.ForeignKey(
ClubAccount,
related_name="labels",
verbose_name=_("club account"),
on_delete=models.CASCADE,
)
class Meta:
unique_together = ("name", "club_account")
def __str__(self):
return "%s (%s)" % (self.name, self.club_account.name)
def get_absolute_url(self):
return reverse(
"accounting:label_list", kwargs={"clubaccount_id": self.club_account.id}
)
def is_owned_by(self, user):
if user.is_anonymous:
return False
return self.club_account.is_owned_by(user)
def can_be_edited_by(self, user):
return self.club_account.can_be_edited_by(user)
def can_be_viewed_by(self, user):
return self.club_account.can_be_viewed_by(user)

View File

@ -1,15 +0,0 @@
from ninja import ModelSchema
from accounting.models import ClubAccount, Company
class ClubAccountSchema(ModelSchema):
class Meta:
model = ClubAccount
fields = ["id", "name"]
class CompanySchema(ModelSchema):
class Meta:
model = Company
fields = ["id", "name"]

View File

@ -1,60 +0,0 @@
import { AjaxSelect } from "#core:core/components/ajax-select-base";
import { registerComponent } from "#core:utils/web-components";
import type { TomOption } from "tom-select/dist/types/types";
import type { escape_html } from "tom-select/dist/types/utils";
import {
type ClubAccountSchema,
type CompanySchema,
accountingSearchClubAccount,
accountingSearchCompany,
} from "#openapi";
@registerComponent("club-account-ajax-select")
export class ClubAccountAjaxSelect extends AjaxSelect {
protected valueField = "id";
protected labelField = "name";
protected searchField = ["code", "name"];
protected async search(query: string): Promise<TomOption[]> {
const resp = await accountingSearchClubAccount({ query: { search: query } });
if (resp.data) {
return resp.data.results;
}
return [];
}
protected renderOption(item: ClubAccountSchema, sanitize: typeof escape_html) {
return `<div class="select-item">
<span class="select-item-text">${sanitize(item.name)}</span>
</div>`;
}
protected renderItem(item: ClubAccountSchema, sanitize: typeof escape_html) {
return `<span>${sanitize(item.name)}</span>`;
}
}
@registerComponent("company-ajax-select")
export class CompanyAjaxSelect extends AjaxSelect {
protected valueField = "id";
protected labelField = "name";
protected searchField = ["code", "name"];
protected async search(query: string): Promise<TomOption[]> {
const resp = await accountingSearchCompany({ query: { search: query } });
if (resp.data) {
return resp.data.results;
}
return [];
}
protected renderOption(item: CompanySchema, sanitize: typeof escape_html) {
return `<div class="select-item">
<span class="select-item-text">${sanitize(item.name)}</span>
</div>`;
}
protected renderItem(item: CompanySchema, sanitize: typeof escape_html) {
return `<span>${sanitize(item.name)}</span>`;
}
}

View File

@ -1,27 +0,0 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Accounting type list{% endtrans %}
{% endblock %}
{% block content %}
<div id="accounting">
<p>
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
{% trans %}Accounting types{% endtrans %}
</p>
<hr>
<p><a href="{{ url('accounting:type_new') }}">{% trans %}New accounting type{% endtrans %}</a></p>
{% if accountingtype_list %}
<h3>{% trans %}Accounting type list{% endtrans %}</h3>
<ul>
{% for a in accountingtype_list %}
<li><a href="{{ url('accounting:type_edit', type_id=a.id) }}">{{ a }}</a></li>
{% endfor %}
</ul>
{% else %}
{% trans %}There is no types in this website.{% endtrans %}
{% endif %}
</div>
{% endblock %}

View File

@ -1,38 +0,0 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Bank account: {% endtrans %}{{ object.name }}
{% endblock %}
{% block content %}
<div id="accounting">
<p>
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
{{ object.name }}
</p>
<hr>
<h2>{% trans %}Bank account: {% endtrans %}{{ object.name }}</h2>
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and not object.club_accounts.exists() %}
<a href="{{ url('accounting:bank_delete', b_account_id=object.id) }}">{% trans %}Delete{% endtrans %}</a>
{% endif %}
<h4>{% trans %}Infos{% endtrans %}</h4>
<ul>
<li><strong>{% trans %}IBAN: {% endtrans %}</strong>{{ object.iban }}</li>
<li><strong>{% trans %}Number: {% endtrans %}</strong>{{ object.number }}</li>
</ul>
<p><a href="{{ url('accounting:club_new') }}?parent={{ object.id }}">{% trans %}New club account{% endtrans %}</a></p>
<ul>
{% for c in object.club_accounts.all() %}
<li><a href="{{ url('accounting:club_details', c_account_id=c.id) }}">{{ c }}</a>
- <a href="{{ url('accounting:club_edit', c_account_id=c.id) }}">{% trans %}Edit{% endtrans %}</a>
{% if c.journals.count() == 0 %}
- <a href="{{ url('accounting:club_delete', c_account_id=c.id) }}">{% trans %}Delete{% endtrans %}</a>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endblock %}

View File

@ -1,33 +0,0 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Bank account list{% endtrans %}
{% endblock %}
{% block content %}
<div id="accounting">
<h4>
{% trans %}Accounting{% endtrans %}
</h4>
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
<p><a href="{{ url('accounting:simple_type_list') }}">{% trans %}Manage simplified types{% endtrans %}</a></p>
<p><a href="{{ url('accounting:type_list') }}">{% trans %}Manage accounting types{% endtrans %}</a></p>
<p><a href="{{ url('accounting:bank_new') }}">{% trans %}New bank account{% endtrans %}</a></p>
{% endif %}
{% if bankaccount_list %}
<h3>{% trans %}Bank account list{% endtrans %}</h3>
<ul>
{% for a in object_list %}
<li><a href="{{ url('accounting:bank_details', b_account_id=a.id) }}">{{ a }}</a>
- <a href="{{ url('accounting:bank_edit', b_account_id=a.id) }}">{% trans %}Edit{% endtrans %}</a>
</li>
{% endfor %}
</ul>
{% else %}
{% trans %}There is no accounts in this website.{% endtrans %}
{% endif %}
</div>
{% endblock %}

View File

@ -1,68 +0,0 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Club account:{% endtrans %} {{ object.name }}
{% endblock %}
{% block content %}
<div id="accounting">
<p>
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
<a href="{{ url('accounting:bank_details', b_account_id=object.bank_account.id) }}">{{object.bank_account }}</a> >
{{ object }}
</p>
<hr>
<h2>{% trans %}Club account:{% endtrans %} {{ object.name }}</h2>
{% if user.is_root and not object.journals.exists() %}
<a href="{{ url('accounting:club_delete', c_account_id=object.id) }}">{% trans %}Delete{% endtrans %}</a>
{% endif %}
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.id }}">{% trans %}New label{% endtrans %}</a></p>
{% endif %}
<p><a href="{{ url('accounting:label_list', clubaccount_id=object.id) }}">{% trans %}Label list{% endtrans %}</a></p>
{% if not object.has_open_journal() %}
<p><a href="{{ url('accounting:journal_new') }}?parent={{ object.id }}">{% trans %}New journal{% endtrans %}</a></p>
{% else %}
<p>{% trans %}You can not create new journal while you still have one opened{% endtrans %}</p>
{% endif %}
<table>
<thead>
<tr>
<td>{% trans %}Name{% endtrans %}</td>
<td>{% trans %}Start{% endtrans %}</td>
<td>{% trans %}End{% endtrans %}</td>
<td>{% trans %}Amount{% endtrans %}</td>
<td>{% trans %}Effective amount{% endtrans %}</td>
<td>{% trans %}Closed{% endtrans %}</td>
<td>{% trans %}Actions{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for j in object.journals.all() %}
<tr>
<td>{{ j.name }}</td>
<td>{{ j.start_date }}</td>
{% if j.end_date %}
<td>{{ j.end_date }}</td>
{% else %}
<td> - </td>
{% endif %}
<td>{{ j.amount }} €</td>
<td>{{ j.effective_amount }} €</td>
{% if j.closed %}
<td>{% trans %}Yes{% endtrans %}</td>
{% else %}
<td>{% trans %}No{% endtrans %}</td>
{% endif %}
<td> <a href="{{ url('accounting:journal_details', j_id=j.id) }}">{% trans %}View{% endtrans %}</a>
<a href="{{ url('accounting:journal_edit', j_id=j.id) }}">{% trans %}Edit{% endtrans %}</a>
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) and j.operations.count() == 0 %}
<a href="{{ url('accounting:journal_delete', j_id=j.id) }}">{% trans %}Delete{% endtrans %}</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -1,30 +0,0 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Company list{% endtrans %}
{% endblock %}
{% block content %}
<div id="accounting">
{% if user.is_root
or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
%}
<p><a href="{{ url('accounting:co_new') }}">{% trans %}Create new company{% endtrans %}</a></p>
{% endif %}
<br/>
<table>
<thead>
<tr>
<td>{% trans %}Companies{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for o in object_list %}
<tr>
<td><a href="{{ url('accounting:co_edit', co_id=o.id) }}">{{ o.get_display_name() }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -1,103 +0,0 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}General journal:{% endtrans %} {{ object.name }}
{% endblock %}
{% block content %}
<div id="accounting">
<p>
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
<a href="{{ url('accounting:bank_details', b_account_id=object.club_account.bank_account.id) }}">{{object.club_account.bank_account }}</a> >
<a href="{{ url('accounting:club_details', c_account_id=object.club_account.id) }}">{{ object.club_account }}</a> >
{{ object.name }}
</p>
<hr>
<h2>{% trans %}General journal:{% endtrans %} {{ object.name }}</h2>
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.club_account.id }}">{% trans %}New label{% endtrans %}</a></p>
<p><a href="{{ url('accounting:label_list', clubaccount_id=object.club_account.id) }}">{% trans %}Label list{% endtrans %}</a></p>
<p><a href="{{ url('accounting:co_list') }}">{% trans %}Company list{% endtrans %}</a></p>
<p><strong>{% trans %}Amount: {% endtrans %}</strong>{{ object.amount }} € -
<strong>{% trans %}Effective amount: {% endtrans %}</strong>{{ object.effective_amount }} €</p>
{% if object.closed %}
<p>{% trans %}Journal is closed, you can not create operation{% endtrans %}</p>
{% else %}
<p><a href="{{ url('accounting:op_new', j_id=object.id) }}">{% trans %}New operation{% endtrans %}</a></p>
</br>
{% endif %}
<div class="journal-table">
<table>
<thead>
<tr>
<td>{% trans %}Nb{% endtrans %}</td>
<td>{% trans %}Date{% endtrans %}</td>
<td>{% trans %}Label{% endtrans %}</td>
<td>{% trans %}Amount{% endtrans %}</td>
<td>{% trans %}Payment mode{% endtrans %}</td>
<td>{% trans %}Target{% endtrans %}</td>
<td>{% trans %}Code{% endtrans %}</td>
<td>{% trans %}Nature{% endtrans %}</td>
<td>{% trans %}Done{% endtrans %}</td>
<td>{% trans %}Comment{% endtrans %}</td>
<td>{% trans %}File{% endtrans %}</td>
<td>{% trans %}Actions{% endtrans %}</td>
<td>{% trans %}PDF{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for o in object.operations.all() %}
<tr>
<td>{{ o.number }}</td>
<td>{{ o.date }}</td>
<td>{{ o.label or "" }}</td>
{% if o.accounting_type.movement_type == "DEBIT" %}
<td class="neg-amount">&nbsp;{{ o.amount }}&nbsp;€</td>
{% else %}
<td class="pos-amount">&nbsp;{{ o.amount }}&nbsp;€</td>
{% endif %}
<td>{{ o.get_mode_display() }}</td>
{% if o.target_type == "OTHER" %}
<td>{{ o.target_label }}</td>
{% else %}
<td><a href="{{ o.target.get_absolute_url() }}">{{ o.target.get_display_name() }}</a></td>
{% endif %}
<td>{{ o.accounting_type.code }}</td>
<td>{{ o.accounting_type.label }}</td>
{% if o.done %}
<td>{% trans %}Yes{% endtrans %}</td>
{% else %}
<td>{% trans %}No{% endtrans %}</td>
{% endif %}
<td>{{ o.remark }}
{% if not o.linked_operation and o.target_type == "ACCOUNT" and not o.target.has_open_journal() %}
<p><strong>
{% trans %}Warning: this operation has no linked operation because the targeted club account has no opened journal.{% endtrans %}
</strong></p>
<p><strong>
{% trans url=o.target.get_absolute_url() %}Open a journal in <a href="{{ url }}">this club account</a>, then save this operation again to make the linked operation.{% endtrans %}
</strong></p>
{% endif %}
</td>
{% if o.invoice %}
<td><a href="{{ url('core:download', file_id=o.invoice.id) }}">{{ o.invoice.name }}</a></td>
{% else %}
<td>-</td>
{% endif %}
<td>
{%
if o.journal.club_account.bank_account.name not in ["AE TI", "TI"]
or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID)
%}
{% if not o.journal.closed %}
<a href="{{ url('accounting:op_edit', op_id=o.id) }}">{% trans %}Edit{% endtrans %}</a>
{% endif %}
{% endif %}
</td>
<td><a href="{{ url('accounting:op_pdf', op_id=o.id) }}">{% trans %}Generate{% endtrans %}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@ -1,33 +0,0 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}General journal:{% endtrans %} {{ object.name }}
{% endblock %}
{% block content %}
<div id="accounting">
<h3>{% trans %}Accounting statement: {% endtrans %} {{ object.name }}</h3>
<table>
<thead>
<tr>
<td>{% trans %}Operation type{% endtrans %}</td>
<td>{% trans %}Sum{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for k,v in statement.items() %}
<tr>
<td>{{ k }}</td>
<td>{{ "%.2f" % v }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><strong>{% trans %}Amount: {% endtrans %}</strong>{{ "%.2f" % object.amount }} €</p>
<p><strong>{% trans %}Effective amount: {% endtrans %}</strong>{{ "%.2f" %object.effective_amount }} €</p>
</div>
{% endblock %}

View File

@ -1,57 +0,0 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}General journal:{% endtrans %} {{ object.name }}
{% endblock %}
{% macro display_tables(dict) %}
<div id="accounting">
<h6>{% trans %}Credit{% endtrans %}</h6>
<table>
<thead>
<tr>
<td>{% trans %}Nature of operation{% endtrans %}</td>
<td>{% trans %}Sum{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for k,v in dict['CREDIT'].items() %}
<tr>
<td>{{ k }}</td>
<td>{{ "%.2f" % v }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% trans %}Total: {% endtrans %}{{ "%.2f" % dict['CREDIT_sum'] }}
<h6>{% trans %}Debit{% endtrans %}</h6>
<table>
<thead>
<tr>
<td>{% trans %}Nature of operation{% endtrans %}</td>
<td>{% trans %}Sum{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for k,v in dict['DEBIT'].items() %}
<tr>
<td>{{ k }}</td>
<td>{{ "%.2f" % v }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% trans %}Total: {% endtrans %}{{ "%.2f" % dict['DEBIT_sum'] }}
{% endmacro %}
{% block content %}
<h3>{% trans %}Statement by nature: {% endtrans %} {{ object.name }}</h3>
{% for k,v in statement.items() %}
<h4 style="background: lightblue; padding: 4px;">{{ k }} : {{ "%.2f" % (v['CREDIT_sum'] - v['DEBIT_sum']) }}</h4>
{{ display_tables(v) }}
<hr>
{% endfor %}
</div>
{% endblock %}

View File

@ -1,68 +0,0 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}General journal:{% endtrans %} {{ object.name }}
{% endblock %}
{% block content %}
<div id="accounting">
<h3>{% trans %}Statement by person: {% endtrans %} {{ object.name }}</h3>
<h4>{% trans %}Credit{% endtrans %}</h4>
<table>
<thead>
<tr>
<td>{% trans %}Target of the operation{% endtrans %}</td>
<td>{% trans %}Sum{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for key in credit_statement.keys() %}
<tr>
{% if key.target_type == "OTHER" %}
<td>{{ o.target_label }}</td>
{% elif key %}
<td><a href="{{ key.get_absolute_url() }}">{{ key.get_display_name() }}</a></td>
{% else %}
<td></td>
{% endif %}
<td>{{ "%.2f" % credit_statement[key] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>Total : {{ "%.2f" % total_credit }}</p>
<h4>{% trans %}Debit{% endtrans %}</h4>
<table>
<thead>
<tr>
<td>{% trans %}Target of the operation{% endtrans %}</td>
<td>{% trans %}Sum{% endtrans %}</td>
</tr>
</thead>
<tbody>
{% for key in debit_statement.keys() %}
<tr>
{% if key.target_type == "OTHER" %}
<td>{{ o.target_label }}</td>
{% elif key %}
<td><a href="{{ key.get_absolute_url() }}">{{ key.get_display_name() }}</a></td>
{% else %}
<td></td>
{% endif %}
<td>{{ "%.2f" % debit_statement[key] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>Total : {{ "%.2f" % total_debit }}</p>
</div>
{% endblock %}

View File

@ -1,36 +0,0 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Label list{% endtrans %}
{% endblock %}
{% block content %}
<div id="accounting">
<p>
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
<a href="{{ url('accounting:bank_details', b_account_id=object.bank_account.id) }}">{{object.bank_account }}</a> >
<a href="{{ url('accounting:club_details', c_account_id=object.id) }}">{{ object }}</a>
</p>
<hr>
<p><a href="{{ url('accounting:club_details', c_account_id=object.id) }}">{% trans %}Back to club account{% endtrans %}</a></p>
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
<p><a href="{{ url('accounting:label_new') }}?parent={{ object.id }}">{% trans %}New label{% endtrans %}</a></p>
{% endif %}
{% if object.labels.all() %}
<h3>{% trans %}Label list{% endtrans %}</h3>
<ul>
{% for l in object.labels.all() %}
<li><a href="{{ url('accounting:label_edit', label_id=l.id) }}">{{ l }}</a>
{% if user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
-
<a href="{{ url('accounting:label_delete', label_id=l.id) }}">{% trans %}Delete{% endtrans %}</a>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
{% trans %}There is no label in this club account.{% endtrans %}
{% endif %}
</div>
{% endblock %}

View File

@ -1,123 +0,0 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Edit operation{% endtrans %}
{% endblock %}
{% block content %}
<div id="accounting">
<p>
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
<a href="{{ url('accounting:bank_details', b_account_id=object.club_account.bank_account.id) }}">{{object.club_account.bank_account }}</a> >
<a href="{{ url('accounting:club_details', c_account_id=object.club_account.id) }}">{{ object.club_account }}</a> >
<a href="{{ url('accounting:journal_details', j_id=object.id) }}">{{ object.name }}</a> >
{% trans %}Edit operation{% endtrans %}
</p>
<hr>
<h2>{% trans %}Edit operation{% endtrans %}</h2>
<form action="" method="post">
{% csrf_token %}
{{ form.non_field_errors() }}
{{ form.journal }}
{{ form.target_id }}
<p>{{ form.amount.errors }}<label for="{{ form.amount.name }}">{{ form.amount.label }}</label> {{ form.amount }}</p>
<p>{{ form.remark.errors }}<label for="{{ form.remark.name }}">{{ form.remark.label }}</label> {{ form.remark }}</p>
<br />
<strong>{% trans %}Warning: if you select <em>Account</em>, the opposite operation will be created in the target account. If you don't want that, select <em>Club</em> instead of <em>Account</em>.{% endtrans %}</strong>
<p>{{ form.target_type.errors }}<label for="{{ form.target_type.name }}">{{ form.target_type.label }}</label> {{ form.target_type }}</p>
{{ form.user }}
{{ form.club }}
{{ form.club_account }}
{{ form.company }}
{{ form.target_label }}
<span id="id_need_link_full"><label>{{ form.need_link.label }}</label> {{ form.need_link }}</span>
<p>{{ form.date.errors }}<label for="{{ form.date.name }}">{{ form.date.label }}</label> {{ form.date }}</p>
<p>{{ form.mode.errors }}<label for="{{ form.mode.name }}">{{ form.mode.label }}</label> {{ form.mode }}</p>
<p>{{ form.cheque_number.errors }}<label for="{{ form.cheque_number.name }}">{{ form.cheque_number.label }}</label> {{
form.cheque_number }}</p>
<p>{{ form.invoice.errors }}<label for="{{ form.invoice.name }}">{{ form.invoice.label }}</label> {{ form.invoice }}</p>
<p>{{ form.simpleaccounting_type.errors }}<label for="{{ form.simpleaccounting_type.name }}">{{
form.simpleaccounting_type.label }}</label> {{ form.simpleaccounting_type }}</p>
<p>{{ form.accounting_type.errors }}<label for="{{ form.accounting_type.name }}">{{ form.accounting_type.label }}</label> {{
form.accounting_type }}</p>
<p>{{ form.label.errors }}<label for="{{ form.label.name }}">{{ form.label.label }}</label> {{ form.label }}</p>
<p>{{ form.done.errors }}<label for="{{ form.done.name }}">{{ form.done.label }}</label> {{ form.done }}</p>
{% if form.instance.linked_operation %}
{% set obj = form.instance.linked_operation %}
<p><strong>{% trans %}Linked operation:{% endtrans %}</strong><br>
<a href="{{ url('accounting:bank_details', b_account_id=obj.journal.club_account.bank_account.id) }}">
{{obj.journal.club_account.bank_account }}</a> >
<a href="{{ url('accounting:club_details', c_account_id=obj.journal.club_account.id) }}">{{ obj.journal.club_account }}</a> >
<a href="{{ url('accounting:journal_details', j_id=obj.journal.id) }}">{{ obj.journal }}</a> >
{{ obj.number }}
</p>
{% endif %}
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
</form>
{% endblock %}
{% block script %}
{{ super() }}
<script>
$( function() {
var target_type = $('#id_target_type');
var user = $('user-ajax-select');
var club = $('club-ajax-select');
var club_account = $('club-account-ajax-select');
var company = $('company-ajax-select');
var other = $('#id_target_label');
var need_link = $('#id_need_link_full');
function update_targets () {
if (target_type.val() == "USER") {
console.log(user);
user.show();
club.hide();
club_account.hide();
company.hide();
other.hide();
need_link.hide();
} else if (target_type.val() == "ACCOUNT") {
club_account.show();
need_link.show();
user.hide();
club.hide();
company.hide();
other.hide();
} else if (target_type.val() == "CLUB") {
club.show();
user.hide();
club_account.hide();
company.hide();
other.hide();
need_link.hide();
} else if (target_type.val() == "COMPANY") {
company.show();
user.hide();
club_account.hide();
club.hide();
other.hide();
need_link.hide();
} else if (target_type.val() == "OTHER") {
other.show();
user.hide();
club.hide();
club_account.hide();
company.hide();
need_link.hide();
} else {
company.hide();
user.hide();
club_account.hide();
club.hide();
other.hide();
need_link.hide();
}
}
update_targets();
target_type.change(update_targets);
} );
</script>
</div>
{% endblock %}

View File

@ -1,27 +0,0 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Simplified type list{% endtrans %}
{% endblock %}
{% block content %}
<div id="accounting">
<p>
<a href="{{ url('accounting:bank_list') }}">{% trans %}Accounting{% endtrans %}</a> >
{% trans %}Simplified types{% endtrans %}
</p>
<hr>
<p><a href="{{ url('accounting:simple_type_new') }}">{% trans %}New simplified type{% endtrans %}</a></p>
{% if simplifiedaccountingtype_list %}
<h3>{% trans %}Simplified type list{% endtrans %}</h3>
<ul>
{% for a in simplifiedaccountingtype_list %}
<li><a href="{{ url('accounting:simple_type_edit', type_id=a.id) }}">{{ a }}</a></li>
{% endfor %}
</ul>
{% else %}
{% trans %}There is no types in this website.{% endtrans %}
{% endif %}
</div>
{% endblock %}

View File

@ -1,292 +0,0 @@
#
# 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"
#
#
from datetime import date, timedelta
from django.test import TestCase
from django.urls import reverse
from accounting.models import (
AccountingType,
GeneralJournal,
Label,
Operation,
SimplifiedAccountingType,
)
from core.models import User
class TestRefoundAccount(TestCase):
@classmethod
def setUpTestData(cls):
cls.skia = User.objects.get(username="skia")
# refill skia's account
cls.skia.customer.amount = 800
cls.skia.customer.save()
cls.refound_account_url = reverse("accounting:refound_account")
def test_permission_denied(self):
self.client.force_login(User.objects.get(username="guy"))
response_post = self.client.post(
self.refound_account_url, {"user": self.skia.id}
)
response_get = self.client.get(self.refound_account_url)
assert response_get.status_code == 403
assert response_post.status_code == 403
def test_root_granteed(self):
self.client.force_login(User.objects.get(username="root"))
response = self.client.post(self.refound_account_url, {"user": self.skia.id})
self.assertRedirects(response, self.refound_account_url)
self.skia.refresh_from_db()
response = self.client.get(self.refound_account_url)
assert response.status_code == 200
assert '<form action="" method="post">' in str(response.content)
assert self.skia.customer.amount == 0
def test_comptable_granteed(self):
self.client.force_login(User.objects.get(username="comptable"))
response = self.client.post(self.refound_account_url, {"user": self.skia.id})
self.assertRedirects(response, self.refound_account_url)
self.skia.refresh_from_db()
response = self.client.get(self.refound_account_url)
assert response.status_code == 200
assert '<form action="" method="post">' in str(response.content)
assert self.skia.customer.amount == 0
class TestJournal(TestCase):
@classmethod
def setUpTestData(cls):
cls.journal = GeneralJournal.objects.get(id=1)
def test_permission_granted(self):
self.client.force_login(User.objects.get(username="comptable"))
response_get = self.client.get(
reverse("accounting:journal_details", args=[self.journal.id])
)
assert response_get.status_code == 200
assert "<td>M\\xc3\\xa9thode de paiement</td>" in str(response_get.content)
def test_permission_not_granted(self):
self.client.force_login(User.objects.get(username="skia"))
response_get = self.client.get(
reverse("accounting:journal_details", args=[self.journal.id])
)
assert response_get.status_code == 403
assert "<td>M\xc3\xa9thode de paiement</td>" not in str(response_get.content)
class TestOperation(TestCase):
def setUp(self):
self.tomorrow_formatted = (date.today() + timedelta(days=1)).strftime(
"%d/%m/%Y"
)
self.journal = GeneralJournal.objects.filter(id=1).first()
self.skia = User.objects.filter(username="skia").first()
at = AccountingType(
code="443", label="Ce code n'existe pas", movement_type="CREDIT"
)
at.save()
label = Label.objects.create(club_account=self.journal.club_account, name="bob")
self.client.force_login(User.objects.get(username="comptable"))
self.op1 = Operation(
journal=self.journal,
date=date.today(),
amount=1,
remark="Test bilan",
mode="CASH",
done=True,
label=label,
accounting_type=at,
target_type="USER",
target_id=self.skia.id,
)
self.op1.save()
self.op2 = Operation(
journal=self.journal,
date=date.today(),
amount=2,
remark="Test bilan",
mode="CASH",
done=True,
label=label,
accounting_type=at,
target_type="USER",
target_id=self.skia.id,
)
self.op2.save()
def test_new_operation(self):
at = AccountingType.objects.get(code="604")
response = self.client.post(
reverse("accounting:op_new", args=[self.journal.id]),
{
"amount": 30,
"remark": "Un gros test",
"journal": self.journal.id,
"target_type": "OTHER",
"target_id": "",
"target_label": "Le fantome de la nuit",
"date": self.tomorrow_formatted,
"mode": "CASH",
"cheque_number": "",
"invoice": "",
"simpleaccounting_type": "",
"accounting_type": at.id,
"label": "",
"done": False,
},
)
self.assertFalse(response.status_code == 403)
self.assertTrue(
self.journal.operations.filter(
target_label="Le fantome de la nuit"
).exists()
)
response_get = self.client.get(
reverse("accounting:journal_details", args=[self.journal.id])
)
self.assertTrue("<td>Le fantome de la nuit</td>" in str(response_get.content))
def test_bad_new_operation(self):
AccountingType.objects.get(code="604")
response = self.client.post(
reverse("accounting:op_new", args=[self.journal.id]),
{
"amount": 30,
"remark": "Un gros test",
"journal": self.journal.id,
"target_type": "OTHER",
"target_id": "",
"target_label": "Le fantome de la nuit",
"date": self.tomorrow_formatted,
"mode": "CASH",
"cheque_number": "",
"invoice": "",
"simpleaccounting_type": "",
"accounting_type": "",
"label": "",
"done": False,
},
)
self.assertTrue(
"Vous devez fournir soit un type comptable simplifi\\xc3\\xa9 ou un type comptable standard"
in str(response.content)
)
def test_new_operation_not_authorized(self):
self.client.force_login(self.skia)
at = AccountingType.objects.filter(code="604").first()
response = self.client.post(
reverse("accounting:op_new", args=[self.journal.id]),
{
"amount": 30,
"remark": "Un gros test",
"journal": self.journal.id,
"target_type": "OTHER",
"target_id": "",
"target_label": "Le fantome du jour",
"date": self.tomorrow_formatted,
"mode": "CASH",
"cheque_number": "",
"invoice": "",
"simpleaccounting_type": "",
"accounting_type": at.id,
"label": "",
"done": False,
},
)
self.assertTrue(response.status_code == 403)
self.assertFalse(
self.journal.operations.filter(target_label="Le fantome du jour").exists()
)
def test_operation_simple_accounting(self):
sat = SimplifiedAccountingType.objects.all().first()
response = self.client.post(
reverse("accounting:op_new", args=[self.journal.id]),
{
"amount": 23,
"remark": "Un gros test",
"journal": self.journal.id,
"target_type": "OTHER",
"target_id": "",
"target_label": "Le fantome de l'aurore",
"date": self.tomorrow_formatted,
"mode": "CASH",
"cheque_number": "",
"invoice": "",
"simpleaccounting_type": sat.id,
"accounting_type": "",
"label": "",
"done": False,
},
)
assert response.status_code != 403
assert self.journal.operations.filter(amount=23).exists()
response_get = self.client.get(
reverse("accounting:journal_details", args=[self.journal.id])
)
assert "<td>Le fantome de l&#39;aurore</td>" in str(response_get.content)
assert (
self.journal.operations.filter(amount=23)
.values("accounting_type")
.first()["accounting_type"]
== AccountingType.objects.filter(code=6).values("id").first()["id"]
)
def test_nature_statement(self):
response = self.client.get(
reverse("accounting:journal_nature_statement", args=[self.journal.id])
)
self.assertContains(response, "bob (Troll Penché) : 3.00", status_code=200)
def test_person_statement(self):
response = self.client.get(
reverse("accounting:journal_person_statement", args=[self.journal.id])
)
self.assertContains(response, "Total : 5575.72", status_code=200)
self.assertContains(response, "Total : 71.42")
content = response.content.decode()
self.assertInHTML(
"""<td><a href="/user/1/">S&#39; Kia</a></td><td>3.00</td>""", content
)
self.assertInHTML(
"""<td><a href="/user/1/">S&#39; Kia</a></td><td>823.00</td>""", content
)
def test_accounting_statement(self):
response = self.client.get(
reverse("accounting:journal_accounting_statement", args=[self.journal.id])
)
assert response.status_code == 200
self.assertInHTML(
"""
<tr>
<td>443 - Crédit - Ce code n&#39;existe pas</td>
<td>3.00</td>
</tr>""",
response.content.decode(),
)
self.assertContains(
response,
"""
<p><strong>Montant : </strong>-5504.30 </p>
<p><strong>Montant effectif: </strong>-5504.30 </p>""",
)

View File

@ -1,173 +0,0 @@
#
# 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"
#
#
from django.urls import path
from accounting.views import (
AccountingTypeCreateView,
AccountingTypeEditView,
AccountingTypeListView,
BankAccountCreateView,
BankAccountDeleteView,
BankAccountDetailView,
BankAccountEditView,
BankAccountListView,
ClubAccountCreateView,
ClubAccountDeleteView,
ClubAccountDetailView,
ClubAccountEditView,
CompanyCreateView,
CompanyEditView,
CompanyListView,
JournalAccountingStatementView,
JournalCreateView,
JournalDeleteView,
JournalDetailView,
JournalEditView,
JournalNatureStatementView,
JournalPersonStatementView,
LabelCreateView,
LabelDeleteView,
LabelEditView,
LabelListView,
OperationCreateView,
OperationEditView,
OperationPDFView,
RefoundAccountView,
SimplifiedAccountingTypeCreateView,
SimplifiedAccountingTypeEditView,
SimplifiedAccountingTypeListView,
)
urlpatterns = [
# Accounting types
path(
"simple_type/",
SimplifiedAccountingTypeListView.as_view(),
name="simple_type_list",
),
path(
"simple_type/create/",
SimplifiedAccountingTypeCreateView.as_view(),
name="simple_type_new",
),
path(
"simple_type/<int:type_id>/edit/",
SimplifiedAccountingTypeEditView.as_view(),
name="simple_type_edit",
),
# Accounting types
path("type/", AccountingTypeListView.as_view(), name="type_list"),
path("type/create/", AccountingTypeCreateView.as_view(), name="type_new"),
path(
"type/<int:type_id>/edit/",
AccountingTypeEditView.as_view(),
name="type_edit",
),
# Bank accounts
path("", BankAccountListView.as_view(), name="bank_list"),
path("bank/create", BankAccountCreateView.as_view(), name="bank_new"),
path(
"bank/<int:b_account_id>/",
BankAccountDetailView.as_view(),
name="bank_details",
),
path(
"bank/<int:b_account_id>/edit/",
BankAccountEditView.as_view(),
name="bank_edit",
),
path(
"bank/<int:b_account_id>/delete/",
BankAccountDeleteView.as_view(),
name="bank_delete",
),
# Club accounts
path("club/create/", ClubAccountCreateView.as_view(), name="club_new"),
path(
"club/<int:c_account_id>/",
ClubAccountDetailView.as_view(),
name="club_details",
),
path(
"club/<int:c_account_id>/edit/",
ClubAccountEditView.as_view(),
name="club_edit",
),
path(
"club/<int:c_account_id>/delete/",
ClubAccountDeleteView.as_view(),
name="club_delete",
),
# Journals
path("journal/create/", JournalCreateView.as_view(), name="journal_new"),
path(
"journal/<int:j_id>/",
JournalDetailView.as_view(),
name="journal_details",
),
path(
"journal/<int:j_id>/edit/",
JournalEditView.as_view(),
name="journal_edit",
),
path(
"journal/<int:j_id>/delete/",
JournalDeleteView.as_view(),
name="journal_delete",
),
path(
"journal/<int:j_id>/statement/nature/",
JournalNatureStatementView.as_view(),
name="journal_nature_statement",
),
path(
"journal/<int:j_id>/statement/person/",
JournalPersonStatementView.as_view(),
name="journal_person_statement",
),
path(
"journal/<int:j_id>/statement/accounting/",
JournalAccountingStatementView.as_view(),
name="journal_accounting_statement",
),
# Operations
path(
"operation/create/<int:j_id>/",
OperationCreateView.as_view(),
name="op_new",
),
path("operation/<int:op_id>/", OperationEditView.as_view(), name="op_edit"),
path("operation/<int:op_id>/pdf/", OperationPDFView.as_view(), name="op_pdf"),
# Companies
path("company/list/", CompanyListView.as_view(), name="co_list"),
path("company/create/", CompanyCreateView.as_view(), name="co_new"),
path("company/<int:co_id>/", CompanyEditView.as_view(), name="co_edit"),
# Labels
path("label/new/", LabelCreateView.as_view(), name="label_new"),
path(
"label/<int:clubaccount_id>/",
LabelListView.as_view(),
name="label_list",
),
path("label/<int:label_id>/edit/", LabelEditView.as_view(), name="label_edit"),
path(
"label/<int:label_id>/delete/",
LabelDeleteView.as_view(),
name="label_delete",
),
# User account
path("refound/account/", RefoundAccountView.as_view(), name="refound_account"),
]

View File

@ -1,885 +0,0 @@
#
# 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, UserPassesTestMixin
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(UserPassesTestMixin, FormView):
"""Create a selling with the same amount than the current user money."""
template_name = "accounting/refound_account.jinja"
form_class = CloseCustomerAccountForm
def test_func(self):
return self.request.user.is_root or self.request.user.is_in_group(
pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID
)
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.request.user,
customer=self.customer.customer,
club=refound_club,
counter=refound_club_counter,
product=Product.objects.get(id=settings.SITH_PRODUCT_REFOUND_ID),
)
s.save()

View File

@ -1,42 +0,0 @@
from pydantic import TypeAdapter
from accounting.models import ClubAccount, Company
from accounting.schemas import ClubAccountSchema, CompanySchema
from core.views.widgets.ajax_select import (
AutoCompleteSelect,
AutoCompleteSelectMultiple,
)
_js = ["bundled/accounting/components/ajax-select-index.ts"]
class AutoCompleteSelectClubAccount(AutoCompleteSelect):
component_name = "club-account-ajax-select"
model = ClubAccount
adapter = TypeAdapter(list[ClubAccountSchema])
js = _js
class AutoCompleteSelectMultipleClubAccount(AutoCompleteSelectMultiple):
component_name = "club-account-ajax-select"
model = ClubAccount
adapter = TypeAdapter(list[ClubAccountSchema])
js = _js
class AutoCompleteSelectCompany(AutoCompleteSelect):
component_name = "company-ajax-select"
model = Company
adapter = TypeAdapter(list[CompanySchema])
js = _js
class AutoCompleteSelectMultipleCompany(AutoCompleteSelectMultiple):
component_name = "company-ajax-select"
model = Company
adapter = TypeAdapter(list[CompanySchema])
js = _js

View File

@ -29,14 +29,6 @@
{% endfor %}
{% endif %}
</ul>
{% if object.club_account.exists() %}
<h4>{% trans %}Accounting: {% endtrans %}</h4>
<ul>
{% for ca in object.club_account.all() %}
<li><a href="{{ url('accounting:club_details', c_account_id=ca.id) }}">{{ ca.get_display_name() }}</a></li>
{% endfor %}
</ul>
{% endif %}
{% if object.unix_name == settings.SITH_LAUNDERETTE_MANAGER['unix_name'] %}
<li><a href="{{ url('launderette:launderette_list') }}">{% trans %}Manage launderettes{% endtrans %}</a></li>
{% endif %}

View File

@ -36,15 +36,6 @@ from django.utils import timezone
from django.utils.timezone import localdate
from PIL import Image
from accounting.models import (
AccountingType,
BankAccount,
ClubAccount,
Company,
GeneralJournal,
Operation,
SimplifiedAccountingType,
)
from club.models import Club, Membership
from com.ics_calendar import IcsCalendar
from com.models import News, NewsDate, Sith, Weekmail
@ -509,6 +500,14 @@ Welcome to the wiki page!
club=main_club,
limit_age=18,
)
Product.objects.create(
name="remboursement",
code="REMBOURS",
purchase_price="0",
selling_price="0",
special_selling_price="0",
club=refound,
)
groups.subscribers.products.add(
cotis, cotis2, refill, barb, cble, cors, carolus
)
@ -521,81 +520,6 @@ Welcome to the wiki page!
eboutic.products.add(barb, cotis, cotis2, refill)
Counter.objects.create(name="Carte AE", club=refound, type="OFFICE")
Product.objects.create(
name="remboursement",
code="REMBOURS",
purchase_price="0",
selling_price="0",
special_selling_price="0",
club=refound,
)
# Accounting test values:
BankAccount.objects.create(name="AE TG", club=main_club)
BankAccount.objects.create(name="Carte AE", club=main_club)
ba = BankAccount.objects.create(name="AE TI", club=main_club)
ca = ClubAccount.objects.create(
name="Troll Penché", bank_account=ba, club=troll
)
gj = GeneralJournal.objects.create(
name="A16", start_date=date.today(), club_account=ca
)
credit = AccountingType.objects.create(
code="74", label="Subventions d'exploitation", movement_type="CREDIT"
)
debit = AccountingType.objects.create(
code="606",
label="Achats non stockés de matières et fournitures(*1)",
movement_type="DEBIT",
)
debit2 = AccountingType.objects.create(
code="604",
label="Achats d'études et prestations de services(*2)",
movement_type="DEBIT",
)
buying = AccountingType.objects.create(
code="60", label="Achats (sauf 603)", movement_type="DEBIT"
)
comptes = AccountingType.objects.create(
code="6", label="Comptes de charge", movement_type="DEBIT"
)
SimplifiedAccountingType.objects.create(
label="Je fais du simple 6", accounting_type=comptes
)
woenzco = Company.objects.create(name="Woenzel & co")
operation_list = [
(27, "J'avais trop de bière", "CASH", buying, "USER", skia.id, None),
(4000, "Pas une opération", "CHECK", debit, "COMPANY", woenzco.id, 23),
(22, "C'est de l'argent ?", "CARD", credit, "CLUB", troll.id, None),
(37, "Je paye CASH", "CASH", debit2, "OTHER", None, None),
(300, "Paiement Guy", "CASH", buying, "USER", skia.id, None),
(32.3, "Essence", "CASH", buying, "OTHER", None, None),
(46.42, "Allumette", "CHECK", credit, "CLUB", main_club.id, 57),
(666.42, "Subvention club", "CASH", comptes, "CLUB", main_club.id, None),
(496, "Ça, c'est un 6", "CARD", comptes, "USER", skia.id, None),
(17, "La Gargotte du Korrigan", "CASH", debit2, "CLUB", bar_club.id, None),
]
operations = [
Operation(
number=index,
journal=gj,
date=localdate(),
amount=op[0],
remark=op[1],
mode=op[2],
done=True,
accounting_type=op[3],
target_type=op[4],
target_id=op[5],
target_label="" if op[4] != "OTHER" else "Autre source",
cheque_number=op[6],
)
for index, op in enumerate(operation_list, start=1)
]
for operation in operations:
operation.clean()
Operation.objects.bulk_create(operations)
# Add barman to counter
Counter.sellers.through.objects.bulk_create(

View File

@ -109,28 +109,8 @@
<h4>{% trans %}Accounting{% endtrans %}</h4>
<ul>
{% if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) %}
<li><a href="{{ url('accounting:refound_account') }}">{% trans %}Refound Account{% endtrans %}</a></li>
<li><a href="{{ url('accounting:bank_list') }}">{% trans %}General accounting{% endtrans %}</a></li>
<li><a href="{{ url('accounting:co_list') }}">{% trans %}Company list{% endtrans %}</a></li>
<li><a href="{{ url("counter:account_refound") }}">{% trans %}Refound Account{% endtrans %}</a></li>
{% endif %}
{% for m in user.memberships.filter(end_date=None).filter(role__gte=7).all() -%}
{%- for b in m.club.bank_accounts.all() %}
<li class="rows">
<strong>{% trans %}Bank account: {% endtrans %}</strong>
<a href="{{ url('accounting:bank_details', b_account_id=b.id) }}">{{ b }}</a>
</li>
{%- endfor %}
{% if m.club.club_account.exists() -%}
{% for ca in m.club.club_account.all() %}
<li class="rows">
<strong>{% trans %}Club account: {% endtrans %}</strong>
<a href="{{ url('accounting:club_details', c_account_id=ca.id) }}">{{ ca }}</a>
</li>
{%- endfor %}
{%- endif -%}
{%- endfor %}
</ul>
</div>
{% endif %}

View File

@ -65,7 +65,7 @@ from core.views.forms import (
UserProfileForm,
)
from core.views.mixins import QuickNotifMixin, TabedViewMixin
from counter.models import Refilling, Selling
from counter.models import Counter, Refilling, Selling
from eboutic.models import Invoice
from subscription.models import Subscription
from trombi.views import UserTrombiForm
@ -385,8 +385,6 @@ class UserStatsView(UserTabsMixin, CanViewMixin, DetailView):
kwargs = super().get_context_data(**kwargs)
from django.db.models import Sum
from counter.models import Counter
foyer = Counter.objects.filter(name="Foyer").first()
mde = Counter.objects.filter(name="MDE").first()
gommette = Counter.objects.filter(name="La Gommette").first()

30
counter/fields.py Normal file
View File

@ -0,0 +1,30 @@
from decimal import Decimal
from django.conf import settings
from django.db import models
class CurrencyField(models.DecimalField):
"""Custom database field used for currency."""
def __init__(self, *args, **kwargs):
kwargs["max_digits"] = 12
kwargs["decimal_places"] = 2
super().__init__(*args, **kwargs)
def to_python(self, value):
if value is None:
return None
return super().to_python(value).quantize(Decimal("0.01"))
if settings.TESTING:
from model_bakery import baker
baker.generators.add(
CurrencyField,
lambda: baker.random_gen.gen_decimal(max_digits=8, decimal_places=2),
)
else: # pragma: no cover
# baker is only used in tests, so we don't need coverage for this part
pass

View File

@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
from phonenumber_field.widgets import RegionalPhoneNumberWidget
from club.widgets.ajax_select import AutoCompleteSelectClub
from core.models import User
from core.views.forms import NFCTextInput, SelectDate, SelectDateTime
from core.views.widgets.ajax_select import (
AutoCompleteSelect,
@ -230,3 +231,13 @@ class EticketForm(forms.ModelForm):
"product": AutoCompleteSelectProduct,
"event_date": SelectDate,
}
class CloseCustomerAccountForm(forms.Form):
user = forms.ModelChoiceField(
label=_("Refound this account"),
help_text=None,
required=True,
widget=AutoCompleteSelectUser,
queryset=User.objects.all(),
)

View File

@ -11,7 +11,7 @@ from django.utils.timezone import now
from django.utils.translation import gettext as _
from core.models import User, UserQuerySet
from counter.models import AccountDump, Counter, Customer, Selling
from counter.models import AccountDump, Counter, Customer, Product, Selling
class Command(BaseCommand):
@ -106,6 +106,7 @@ class Command(BaseCommand):
raise ValueError("One or more accounts were not engaged in a dump process")
counter = Counter.objects.get(pk=settings.SITH_COUNTER_ACCOUNT_DUMP_ID)
seller = User.objects.get(pk=settings.SITH_ROOT_USER_ID)
product = Product.objects.get(id=settings.SITH_PRODUCT_REFOUND_ID)
sales = Selling.objects.bulk_create(
[
Selling(
@ -113,7 +114,7 @@ class Command(BaseCommand):
club=counter.club,
counter=counter,
seller=seller,
product=None,
product=product,
customer=account,
quantity=1,
unit_price=account.amount,
@ -126,7 +127,7 @@ class Command(BaseCommand):
sales.sort(key=attrgetter("customer_id"))
# dumps and sales are linked to the same customers
# and or both ordered with the same key, so zipping them is valid
# and both ordered with the same key, so zipping them is valid
for dump, sale in zip(pending_dumps, sales, strict=False):
dump.dump_operation = sale
AccountDump.objects.bulk_update(pending_dumps, ["dump_operation"])

View File

@ -4,7 +4,7 @@ import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import accounting.models
import counter.fields
class Migration(migrations.Migration):
@ -78,7 +78,7 @@ class Migration(migrations.Migration):
),
(
"amount",
accounting.models.CurrencyField(
counter.fields.CurrencyField(
decimal_places=2, max_digits=12, verbose_name="amount"
),
),
@ -145,19 +145,19 @@ class Migration(migrations.Migration):
),
(
"purchase_price",
accounting.models.CurrencyField(
counter.fields.CurrencyField(
decimal_places=2, max_digits=12, verbose_name="purchase price"
),
),
(
"selling_price",
accounting.models.CurrencyField(
counter.fields.CurrencyField(
decimal_places=2, max_digits=12, verbose_name="selling price"
),
),
(
"special_selling_price",
accounting.models.CurrencyField(
counter.fields.CurrencyField(
decimal_places=2,
max_digits=12,
verbose_name="special selling price",
@ -240,7 +240,7 @@ class Migration(migrations.Migration):
),
(
"amount",
accounting.models.CurrencyField(
counter.fields.CurrencyField(
decimal_places=2, max_digits=12, verbose_name="amount"
),
),
@ -324,7 +324,7 @@ class Migration(migrations.Migration):
("label", models.CharField(max_length=64, verbose_name="label")),
(
"unit_price",
accounting.models.CurrencyField(
counter.fields.CurrencyField(
decimal_places=2, max_digits=12, verbose_name="unit price"
),
),

View File

@ -4,7 +4,7 @@ import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import accounting.models
import counter.fields
class Migration(migrations.Migration):
@ -67,7 +67,7 @@ class Migration(migrations.Migration):
),
(
"value",
accounting.models.CurrencyField(
counter.fields.CurrencyField(
max_digits=12, verbose_name="value", decimal_places=2
),
),

View File

@ -2,7 +2,7 @@
from django.db import migrations
import accounting.models
import counter.fields
class Migration(migrations.Migration):
@ -14,7 +14,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="customer",
name="amount",
field=accounting.models.CurrencyField(
field=counter.fields.CurrencyField(
decimal_places=2, default=0, max_digits=12, verbose_name="amount"
),
),

View File

@ -2,7 +2,7 @@
from django.db import migrations, models
import accounting.models
import counter.fields
class Migration(migrations.Migration):
@ -18,7 +18,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="product",
name="purchase_price",
field=accounting.models.CurrencyField(
field=counter.fields.CurrencyField(
decimal_places=2,
help_text="Initial cost of purchasing the product",
max_digits=12,
@ -28,7 +28,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name="product",
name="special_selling_price",
field=accounting.models.CurrencyField(
field=counter.fields.CurrencyField(
decimal_places=2,
help_text="Price for barmen during their permanence",
max_digits=12,

View File

@ -38,12 +38,12 @@ from django_countries.fields import CountryField
from ordered_model.models import OrderedModel
from phonenumber_field.modelfields import PhoneNumberField
from accounting.models import CurrencyField
from club.models import Club
from core.fields import ResizedImageField
from core.models import Group, Notification, User
from core.utils import get_start_of_semester
from counter.apps import PAYMENT_METHOD
from counter.fields import CurrencyField
from sith.settings import SITH_MAIN_CLUB
from subscription.models import Subscription

View File

@ -0,0 +1,59 @@
#
# 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"
#
#
from django.test import TestCase
from django.urls import reverse
from core.models import User
class TestRefoundAccount(TestCase):
@classmethod
def setUpTestData(cls):
cls.skia = User.objects.get(username="skia")
# refill skia's account
cls.skia.customer.amount = 800
cls.skia.customer.save()
cls.refound_account_url = reverse("counter:account_refound")
def test_permission_denied(self):
self.client.force_login(User.objects.get(username="guy"))
response_post = self.client.post(
self.refound_account_url, {"user": self.skia.id}
)
response_get = self.client.get(self.refound_account_url)
assert response_get.status_code == 403
assert response_post.status_code == 403
def test_root_granteed(self):
self.client.force_login(User.objects.get(username="root"))
response = self.client.post(self.refound_account_url, {"user": self.skia.id})
self.assertRedirects(response, self.refound_account_url)
self.skia.refresh_from_db()
response = self.client.get(self.refound_account_url)
assert response.status_code == 200
assert '<form action="" method="post">' in str(response.content)
assert self.skia.customer.amount == 0
def test_comptable_granteed(self):
self.client.force_login(User.objects.get(username="comptable"))
response = self.client.post(self.refound_account_url, {"user": self.skia.id})
self.assertRedirects(response, self.refound_account_url)
self.skia.refresh_from_db()
response = self.client.get(self.refound_account_url)
assert response.status_code == 200
assert '<form action="" method="post">' in str(response.content)
assert self.skia.customer.amount == 0

View File

@ -30,6 +30,7 @@ from counter.views.admin import (
ProductTypeEditView,
ProductTypeListView,
RefillingDeleteView,
RefoundAccountView,
SellingDeleteView,
)
from counter.views.auth import counter_login, counter_logout
@ -51,10 +52,7 @@ from counter.views.home import (
CounterMain,
)
from counter.views.invoice import InvoiceCallView
from counter.views.student_card import (
StudentCardDeleteView,
StudentCardFormView,
)
from counter.views.student_card import StudentCardDeleteView, StudentCardFormView
urlpatterns = [
path("<int:counter_id>/", CounterMain.as_view(), name="details"),
@ -151,4 +149,5 @@ urlpatterns = [
CounterRefillingListView.as_view(),
name="refilling_list",
),
path("admin/refound/", RefoundAccountView.as_view(), name="account_refound"),
]

View File

@ -15,18 +15,21 @@
from datetime import timedelta
from django.conf import settings
from django.contrib.auth.mixins import UserPassesTestMixin
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.forms import CheckboxSelectMultiple
from django.forms.models import modelform_factory
from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext as _
from django.views.generic import DetailView, ListView, TemplateView
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
from core.auth.mixins import CanEditMixin, CanViewMixin
from core.utils import get_semester_code, get_start_of_semester
from counter.forms import CounterEditForm, ProductEditForm
from counter.forms import CloseCustomerAccountForm, CounterEditForm, ProductEditForm
from counter.models import Counter, Product, ProductType, Refilling, Selling
from counter.utils import is_logged_in_counter
from counter.views.mixins import CounterAdminMixin, CounterAdminTabsMixin
@ -253,3 +256,42 @@ class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListVie
kwargs = super().get_context_data(**kwargs)
kwargs["counter"] = self.counter
return kwargs
class RefoundAccountView(UserPassesTestMixin, FormView):
"""Create a selling with the same amount as the current user money."""
template_name = "counter/refound_account.jinja"
form_class = CloseCustomerAccountForm
def test_func(self):
return self.request.user.is_root or self.request.user.is_in_group(
pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID
)
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 self.request.path
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.request.user,
customer=self.customer.customer,
club=refound_club,
counter=refound_club_counter,
product=Product.objects.get(id=settings.SITH_PRODUCT_REFOUND_ID),
)
s.save()

View File

@ -19,7 +19,7 @@ from django.db.models import F
from django.utils import timezone
from django.views.generic import TemplateView
from accounting.models import CurrencyField
from counter.fields import CurrencyField
from counter.models import Refilling, Selling
from counter.views.mixins import CounterAdminMixin, CounterAdminTabsMixin

View File

@ -4,7 +4,7 @@ import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import accounting.models
import counter.fields
class Migration(migrations.Migration):
@ -55,7 +55,7 @@ class Migration(migrations.Migration):
("type_id", models.IntegerField(verbose_name="product type id")),
(
"product_unit_price",
accounting.models.CurrencyField(
counter.fields.CurrencyField(
decimal_places=2, max_digits=12, verbose_name="unit price"
),
),
@ -120,7 +120,7 @@ class Migration(migrations.Migration):
("type_id", models.IntegerField(verbose_name="product type id")),
(
"product_unit_price",
accounting.models.CurrencyField(
counter.fields.CurrencyField(
decimal_places=2, max_digits=12, verbose_name="unit price"
),
),

View File

@ -25,8 +25,8 @@ from django.db.models import F, OuterRef, Subquery, Sum
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from accounting.models import CurrencyField
from core.models import User
from counter.fields import CurrencyField
from counter.models import BillingInfo, Counter, Customer, Product, Refilling, Selling

File diff suppressed because it is too large Load Diff

View File

@ -41,10 +41,6 @@ urlpatterns = [
path("com/", include(("com.urls", "com"), namespace="com")),
path("club/", include(("club.urls", "club"), namespace="club")),
path("counter/", include(("counter.urls", "counter"), namespace="counter")),
path(
"accounting/",
include(("accounting.urls", "accounting"), namespace="accounting"),
),
path("eboutic/", include(("eboutic.urls", "eboutic"), namespace="eboutic")),
path(
"launderette/",