diff --git a/.gitignore b/.gitignore index c350ed9e..76fd8b83 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ db.sqlite3 *.mo *__pycache__* .DS_Store +.vscode/ env/ doc/html data/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 30b00fce..57f7f4ec 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,8 @@ +stages: + - test + test: + stage: test script: - apt-get update - apt-get install -y gettext @@ -11,3 +15,9 @@ test: artifacts: paths: - coverage_report/ + +black: + stage: test + script: + - pip install black + - black --check . diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9dc4aeab..3d68371e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Hey ! Tu veux devenir un mec bien et en plus devenir bon en python si tu l'es pas déjà ? Il se trouve que le sith AE prévu pour l'été 2016 a besoin de toi ! -Pour faire le sith, on utilise le framework Web [Django](https://docs.djangoproject.com/fr/1.8/intro/) +Pour faire le sith, on utilise le framework Web [Django](https://docs.djangoproject.com/fr/1.11/intro/) N'hésite pas à lire les tutos et à nous demander (ae.info@utbm.fr). Bon, passons aux choses sérieuses, pour bidouiller le sith sans le casser : @@ -17,31 +17,81 @@ Ensuite, tu fais : `git clone https://ae-dev.utbm.fr/ae/Sith.git` Avec cette commande, tu clones le sith AE dans le dossier courant. +```bash cd Sith virtualenv --clear --python=python3 env_sith source env_sith/bin/activate pip install -r requirements.txt +``` Maintenant, faut passer le sith en mode debug dans le fichier de settings personnalisé. +```bash echo "DEBUG=True" > sith/settings_custom.py echo 'EXTERNAL_RES = "False"' >> sith/settings_custom.py echo 'SITH_URL = "localhost:8000"' >> sith/settings_custom.py +``` Enfin, il s'agit de créer la base de donnée de test lors de la première utilisation +```bash ./manage.py setup - répondre no +``` Et pour lancer le sith, tu fais `python3 manage.py runserver` Voilà, c'est le sith AE. Il y a des issues dans le gitlab qui sont à régler. Si tu as un domaine qui t'intéresse, une appli que tu voudrais développer, n'hésites pas et contacte-nous. Va, et que l'AE soit avec toi. +# Black + +Pour uniformiser le formattage du code nous utilisons [Black](https://github.com/ambv/black). Cela permet d'avoir le même codestyle et donc le codereview prend moins de temps. Tout étant dans le même format, il est plus facile pour chacun de comprendre le code de chacun ! Cela permet aussi d'éviter des erreurs (y parait 🤷♀️). + +Installation de black: + +```bash + pip install black +``` + +## Sous VsCode: +Attention, pour VsCode, Black doit être installé dans votre virtualenv ! +Ajouter ces deux lignes dans les settings de VsCode + +```json +{ + "python.formatting.provider": "black", + "editor.formatOnSave": true +} +``` + +## Sous Sublime Text +Il faut installer le plugin [sublack](https://packagecontrol.io/packages/sublack) depuis Package Control. + +Il suffit ensuite d'ajouter dans les settings du projet (ou en global) + +```json +{ + "sublack.black_on_save": true +} +``` + +Si vous utilisez le plugin [anaconda](http://damnwidget.github.io/anaconda/), pensez à modifier les paramètres du linter pep8 pour éviter de recevoir des warnings dans le formatage de black + +```json +{ + "pep8_ignore": [ + "E203", + "E266", + "E501", + "W503" + ] +} +``` + Sites et doc cools ------------------ -[Classy Class-Based Views](http://ccbv.co.uk/projects/Django/1.8/) +[Classy Class-Based Views](http://ccbv.co.uk/projects/Django/1.11/) Helpers: diff --git a/README.md b/README.md index b82e3a49..e4e66955 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![pipeline status](https://ae-dev.utbm.fr/ae/Sith/badges/master/pipeline.svg)](https://ae-dev.utbm.fr/ae/Sith/commits/master) [![coverage report](https://ae-dev.utbm.fr/ae/Sith/badges/master/coverage.svg)](https://ae-dev.utbm.fr/ae/Sith/commits/master) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) ## Sith AE diff --git a/accounting/__init__.py b/accounting/__init__.py index 0a9419f8..0ace29c4 100644 --- a/accounting/__init__.py +++ b/accounting/__init__.py @@ -21,4 +21,3 @@ # Place - Suite 330, Boston, MA 02111-1307, USA. # # - diff --git a/accounting/migrations/0001_initial.py b/accounting/migrations/0001_initial.py index 19b967bc..9bd829a6 100644 --- a/accounting/migrations/0001_initial.py +++ b/accounting/migrations/0001_initial.py @@ -8,103 +8,271 @@ import accounting.models class Migration(migrations.Migration): - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='AccountingType', + name="AccountingType", fields=[ - ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), - ('code', models.CharField(max_length=16, verbose_name='code', validators=[django.core.validators.RegexValidator('^[0-9]*$', 'An accounting type code contains only numbers')])), - ('label', models.CharField(max_length=128, verbose_name='label')), - ('movement_type', models.CharField(choices=[('CREDIT', 'Credit'), ('DEBIT', 'Debit'), ('NEUTRAL', 'Neutral')], max_length=12, verbose_name='movement type')), + ( + "id", + models.AutoField( + primary_key=True, + serialize=False, + verbose_name="ID", + auto_created=True, + ), + ), + ( + "code", + models.CharField( + max_length=16, + verbose_name="code", + validators=[ + django.core.validators.RegexValidator( + "^[0-9]*$", + "An accounting type code contains only numbers", + ) + ], + ), + ), + ("label", models.CharField(max_length=128, verbose_name="label")), + ( + "movement_type", + models.CharField( + choices=[ + ("CREDIT", "Credit"), + ("DEBIT", "Debit"), + ("NEUTRAL", "Neutral"), + ], + max_length=12, + verbose_name="movement type", + ), + ), ], options={ - 'verbose_name': 'accounting type', - 'ordering': ['movement_type', 'code'], + "verbose_name": "accounting type", + "ordering": ["movement_type", "code"], }, ), migrations.CreateModel( - name='BankAccount', + name="BankAccount", fields=[ - ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), - ('name', models.CharField(max_length=30, verbose_name='name')), - ('iban', models.CharField(max_length=255, blank=True, verbose_name='iban')), - ('number', models.CharField(max_length=255, blank=True, verbose_name='account number')), + ( + "id", + models.AutoField( + primary_key=True, + serialize=False, + verbose_name="ID", + auto_created=True, + ), + ), + ("name", models.CharField(max_length=30, verbose_name="name")), + ( + "iban", + models.CharField(max_length=255, blank=True, verbose_name="iban"), + ), + ( + "number", + models.CharField( + max_length=255, blank=True, verbose_name="account number" + ), + ), + ], + options={"verbose_name": "Bank account", "ordering": ["club", "name"]}, + ), + migrations.CreateModel( + name="ClubAccount", + fields=[ + ( + "id", + models.AutoField( + primary_key=True, + serialize=False, + verbose_name="ID", + auto_created=True, + ), + ), + ("name", models.CharField(max_length=30, verbose_name="name")), ], options={ - 'verbose_name': 'Bank account', - 'ordering': ['club', 'name'], + "verbose_name": "Club account", + "ordering": ["bank_account", "name"], }, ), migrations.CreateModel( - name='ClubAccount', + name="Company", fields=[ - ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), - ('name', models.CharField(max_length=30, verbose_name='name')), + ( + "id", + models.AutoField( + primary_key=True, + serialize=False, + verbose_name="ID", + auto_created=True, + ), + ), + ("name", models.CharField(max_length=60, verbose_name="name")), ], - options={ - 'verbose_name': 'Club account', - 'ordering': ['bank_account', 'name'], - }, + options={"verbose_name": "company"}, ), migrations.CreateModel( - name='Company', + name="GeneralJournal", fields=[ - ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), - ('name', models.CharField(max_length=60, verbose_name='name')), + ( + "id", + models.AutoField( + primary_key=True, + serialize=False, + verbose_name="ID", + auto_created=True, + ), + ), + ("start_date", models.DateField(verbose_name="start date")), + ( + "end_date", + models.DateField( + null=True, verbose_name="end date", default=None, blank=True + ), + ), + ("name", models.CharField(max_length=40, verbose_name="name")), + ( + "closed", + models.BooleanField(verbose_name="is closed", default=False), + ), + ( + "amount", + accounting.models.CurrencyField( + decimal_places=2, + default=0, + verbose_name="amount", + max_digits=12, + ), + ), + ( + "effective_amount", + accounting.models.CurrencyField( + decimal_places=2, + default=0, + verbose_name="effective_amount", + max_digits=12, + ), + ), ], - options={ - 'verbose_name': 'company', - }, + options={"verbose_name": "General journal", "ordering": ["-start_date"]}, ), migrations.CreateModel( - name='GeneralJournal', + name="Operation", fields=[ - ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), - ('start_date', models.DateField(verbose_name='start date')), - ('end_date', models.DateField(null=True, verbose_name='end date', default=None, blank=True)), - ('name', models.CharField(max_length=40, verbose_name='name')), - ('closed', models.BooleanField(verbose_name='is closed', default=False)), - ('amount', accounting.models.CurrencyField(decimal_places=2, default=0, verbose_name='amount', max_digits=12)), - ('effective_amount', accounting.models.CurrencyField(decimal_places=2, default=0, verbose_name='effective_amount', max_digits=12)), + ( + "id", + models.AutoField( + primary_key=True, + serialize=False, + verbose_name="ID", + auto_created=True, + ), + ), + ("number", models.IntegerField(verbose_name="number")), + ( + "amount", + accounting.models.CurrencyField( + decimal_places=2, max_digits=12, verbose_name="amount" + ), + ), + ("date", models.DateField(verbose_name="date")), + ("remark", models.CharField(max_length=128, verbose_name="comment")), + ( + "mode", + models.CharField( + choices=[ + ("CHECK", "Check"), + ("CASH", "Cash"), + ("TRANSFERT", "Transfert"), + ("CARD", "Credit card"), + ], + max_length=255, + verbose_name="payment method", + ), + ), + ( + "cheque_number", + models.CharField( + max_length=32, + null=True, + verbose_name="cheque number", + default="", + blank=True, + ), + ), + ("done", models.BooleanField(verbose_name="is done", default=False)), + ( + "target_type", + models.CharField( + choices=[ + ("USER", "User"), + ("CLUB", "Club"), + ("ACCOUNT", "Account"), + ("COMPANY", "Company"), + ("OTHER", "Other"), + ], + max_length=10, + verbose_name="target type", + ), + ), + ( + "target_id", + models.IntegerField( + null=True, verbose_name="target id", blank=True + ), + ), + ( + "target_label", + models.CharField( + max_length=32, + blank=True, + verbose_name="target label", + default="", + ), + ), + ( + "accounting_type", + models.ForeignKey( + null=True, + related_name="operations", + verbose_name="accounting type", + to="accounting.AccountingType", + blank=True, + ), + ), ], - options={ - 'verbose_name': 'General journal', - 'ordering': ['-start_date'], - }, + options={"ordering": ["-number"]}, ), migrations.CreateModel( - name='Operation', + name="SimplifiedAccountingType", fields=[ - ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), - ('number', models.IntegerField(verbose_name='number')), - ('amount', accounting.models.CurrencyField(decimal_places=2, max_digits=12, verbose_name='amount')), - ('date', models.DateField(verbose_name='date')), - ('remark', models.CharField(max_length=128, verbose_name='comment')), - ('mode', models.CharField(choices=[('CHECK', 'Check'), ('CASH', 'Cash'), ('TRANSFERT', 'Transfert'), ('CARD', 'Credit card')], max_length=255, verbose_name='payment method')), - ('cheque_number', models.CharField(max_length=32, null=True, verbose_name='cheque number', default='', blank=True)), - ('done', models.BooleanField(verbose_name='is done', default=False)), - ('target_type', models.CharField(choices=[('USER', 'User'), ('CLUB', 'Club'), ('ACCOUNT', 'Account'), ('COMPANY', 'Company'), ('OTHER', 'Other')], max_length=10, verbose_name='target type')), - ('target_id', models.IntegerField(null=True, verbose_name='target id', blank=True)), - ('target_label', models.CharField(max_length=32, blank=True, verbose_name='target label', default='')), - ('accounting_type', models.ForeignKey(null=True, related_name='operations', verbose_name='accounting type', to='accounting.AccountingType', blank=True)), + ( + "id", + models.AutoField( + primary_key=True, + serialize=False, + verbose_name="ID", + auto_created=True, + ), + ), + ("label", models.CharField(max_length=128, verbose_name="label")), + ( + "accounting_type", + models.ForeignKey( + verbose_name="simplified accounting types", + to="accounting.AccountingType", + related_name="simplified_types", + ), + ), ], options={ - 'ordering': ['-number'], - }, - ), - migrations.CreateModel( - name='SimplifiedAccountingType', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), - ('label', models.CharField(max_length=128, verbose_name='label')), - ('accounting_type', models.ForeignKey(verbose_name='simplified accounting types', to='accounting.AccountingType', related_name='simplified_types')), - ], - options={ - 'verbose_name': 'simplified type', - 'ordering': ['accounting_type__movement_type', 'accounting_type__code'], + "verbose_name": "simplified type", + "ordering": ["accounting_type__movement_type", "accounting_type__code"], }, ), ] diff --git a/accounting/migrations/0002_auto_20160824_2152.py b/accounting/migrations/0002_auto_20160824_2152.py index b44310ff..e0322f08 100644 --- a/accounting/migrations/0002_auto_20160824_2152.py +++ b/accounting/migrations/0002_auto_20160824_2152.py @@ -7,54 +7,88 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('club', '0001_initial'), - ('accounting', '0001_initial'), - ('core', '0001_initial'), + ("club", "0001_initial"), + ("accounting", "0001_initial"), + ("core", "0001_initial"), ] operations = [ migrations.AddField( - model_name='operation', - name='invoice', - field=models.ForeignKey(null=True, related_name='operations', verbose_name='invoice', to='core.SithFile', blank=True), + model_name="operation", + name="invoice", + field=models.ForeignKey( + null=True, + related_name="operations", + verbose_name="invoice", + to="core.SithFile", + blank=True, + ), ), migrations.AddField( - model_name='operation', - name='journal', - field=models.ForeignKey(verbose_name='journal', to='accounting.GeneralJournal', related_name='operations'), + model_name="operation", + name="journal", + field=models.ForeignKey( + verbose_name="journal", + to="accounting.GeneralJournal", + related_name="operations", + ), ), migrations.AddField( - model_name='operation', - name='linked_operation', - field=models.OneToOneField(blank=True, to='accounting.Operation', null=True, related_name='operation_linked_to', verbose_name='linked operation', default=None), + model_name="operation", + name="linked_operation", + field=models.OneToOneField( + blank=True, + to="accounting.Operation", + null=True, + related_name="operation_linked_to", + verbose_name="linked operation", + default=None, + ), ), migrations.AddField( - model_name='operation', - name='simpleaccounting_type', - field=models.ForeignKey(null=True, related_name='operations', verbose_name='simple type', to='accounting.SimplifiedAccountingType', blank=True), + model_name="operation", + name="simpleaccounting_type", + field=models.ForeignKey( + null=True, + related_name="operations", + verbose_name="simple type", + to="accounting.SimplifiedAccountingType", + blank=True, + ), ), migrations.AddField( - model_name='generaljournal', - name='club_account', - field=models.ForeignKey(verbose_name='club account', to='accounting.ClubAccount', related_name='journals'), + model_name="generaljournal", + name="club_account", + field=models.ForeignKey( + verbose_name="club account", + to="accounting.ClubAccount", + related_name="journals", + ), ), migrations.AddField( - model_name='clubaccount', - name='bank_account', - field=models.ForeignKey(verbose_name='bank account', to='accounting.BankAccount', related_name='club_accounts'), + model_name="clubaccount", + name="bank_account", + field=models.ForeignKey( + verbose_name="bank account", + to="accounting.BankAccount", + related_name="club_accounts", + ), ), migrations.AddField( - model_name='clubaccount', - name='club', - field=models.ForeignKey(verbose_name='club', to='club.Club', related_name='club_account'), + model_name="clubaccount", + name="club", + field=models.ForeignKey( + verbose_name="club", to="club.Club", related_name="club_account" + ), ), migrations.AddField( - model_name='bankaccount', - name='club', - field=models.ForeignKey(verbose_name='club', to='club.Club', related_name='bank_accounts'), + model_name="bankaccount", + name="club", + field=models.ForeignKey( + verbose_name="club", to="club.Club", related_name="bank_accounts" + ), ), migrations.AlterUniqueTogether( - name='operation', - unique_together=set([('number', 'journal')]), + name="operation", unique_together=set([("number", "journal")]) ), ] diff --git a/accounting/migrations/0003_auto_20160824_2203.py b/accounting/migrations/0003_auto_20160824_2203.py index 57bd9e22..47ae232f 100644 --- a/accounting/migrations/0003_auto_20160824_2203.py +++ b/accounting/migrations/0003_auto_20160824_2203.py @@ -7,44 +7,44 @@ import phonenumber_field.modelfields class Migration(migrations.Migration): - dependencies = [ - ('accounting', '0002_auto_20160824_2152'), - ] + dependencies = [("accounting", "0002_auto_20160824_2152")] operations = [ migrations.AddField( - model_name='company', - name='city', - field=models.CharField(blank=True, verbose_name='city', max_length=60), + model_name="company", + name="city", + field=models.CharField(blank=True, verbose_name="city", max_length=60), ), migrations.AddField( - model_name='company', - name='country', - field=models.CharField(blank=True, verbose_name='country', max_length=32), + model_name="company", + name="country", + field=models.CharField(blank=True, verbose_name="country", max_length=32), ), migrations.AddField( - model_name='company', - name='email', - field=models.EmailField(blank=True, verbose_name='email', max_length=254), + model_name="company", + name="email", + field=models.EmailField(blank=True, verbose_name="email", max_length=254), ), migrations.AddField( - model_name='company', - name='phone', - field=phonenumber_field.modelfields.PhoneNumberField(blank=True, verbose_name='phone', max_length=128), + model_name="company", + name="phone", + field=phonenumber_field.modelfields.PhoneNumberField( + blank=True, verbose_name="phone", max_length=128 + ), ), migrations.AddField( - model_name='company', - name='postcode', - field=models.CharField(blank=True, verbose_name='postcode', max_length=10), + model_name="company", + name="postcode", + field=models.CharField(blank=True, verbose_name="postcode", max_length=10), ), migrations.AddField( - model_name='company', - name='street', - field=models.CharField(blank=True, verbose_name='street', max_length=60), + model_name="company", + name="street", + field=models.CharField(blank=True, verbose_name="street", max_length=60), ), migrations.AddField( - model_name='company', - name='website', - field=models.CharField(blank=True, verbose_name='website', max_length=64), + model_name="company", + name="website", + field=models.CharField(blank=True, verbose_name="website", max_length=64), ), ] diff --git a/accounting/migrations/0004_auto_20161005_1505.py b/accounting/migrations/0004_auto_20161005_1505.py index b1bfe2d8..c363cddb 100644 --- a/accounting/migrations/0004_auto_20161005_1505.py +++ b/accounting/migrations/0004_auto_20161005_1505.py @@ -7,26 +7,45 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('accounting', '0003_auto_20160824_2203'), - ] + dependencies = [("accounting", "0003_auto_20160824_2203")] operations = [ migrations.CreateModel( - name='Label', + name="Label", fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), - ('name', models.CharField(max_length=64, verbose_name='label')), - ('club_account', models.ForeignKey(related_name='labels', verbose_name='club account', to='accounting.ClubAccount')), + ( + "id", + models.AutoField( + verbose_name="ID", + primary_key=True, + auto_created=True, + serialize=False, + ), + ), + ("name", models.CharField(max_length=64, verbose_name="label")), + ( + "club_account", + models.ForeignKey( + related_name="labels", + verbose_name="club account", + to="accounting.ClubAccount", + ), + ), ], ), migrations.AddField( - model_name='operation', - name='label', - field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, related_name='operations', null=True, blank=True, verbose_name='label', to='accounting.Label'), + model_name="operation", + name="label", + field=models.ForeignKey( + on_delete=django.db.models.deletion.SET_NULL, + related_name="operations", + null=True, + blank=True, + verbose_name="label", + to="accounting.Label", + ), ), migrations.AlterUniqueTogether( - name='label', - unique_together=set([('name', 'club_account')]), + name="label", unique_together=set([("name", "club_account")]) ), ] diff --git a/accounting/migrations/0005_auto_20170324_0917.py b/accounting/migrations/0005_auto_20170324_0917.py index d97d82f0..c8258f8e 100644 --- a/accounting/migrations/0005_auto_20170324_0917.py +++ b/accounting/migrations/0005_auto_20170324_0917.py @@ -6,14 +6,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('accounting', '0004_auto_20161005_1505'), - ] + dependencies = [("accounting", "0004_auto_20161005_1505")] operations = [ migrations.AlterField( - model_name='operation', - name='remark', - field=models.CharField(null=True, max_length=128, blank=True, verbose_name='comment'), - ), + model_name="operation", + name="remark", + field=models.CharField( + null=True, max_length=128, blank=True, verbose_name="comment" + ), + ) ] diff --git a/accounting/models.py b/accounting/models.py index e5d73652..efcbdf2c 100644 --- a/accounting/models.py +++ b/accounting/models.py @@ -41,9 +41,10 @@ class CurrencyField(models.DecimalField): """ This is a custom database field used for currency """ + def __init__(self, *args, **kwargs): - kwargs['max_digits'] = 12 - kwargs['decimal_places'] = 2 + kwargs["max_digits"] = 12 + kwargs["decimal_places"] = 2 super(CurrencyField, self).__init__(*args, **kwargs) def to_python(self, value): @@ -52,18 +53,19 @@ class CurrencyField(models.DecimalField): except AttributeError: return None + # 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) + 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") @@ -81,7 +83,7 @@ class Company(models.Model): Method to see if that object can be edited by the given user """ for club in user.memberships.filter(end_date=None).all(): - if club and club.role == settings.SITH_CLUB_ROLES_ID['Treasurer']: + if club and club.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]: return True return False @@ -90,12 +92,12 @@ class Company(models.Model): Method to see if that object can be viewed by the given user """ for club in user.memberships.filter(end_date=None).all(): - if club and club.role >= settings.SITH_CLUB_ROLES_ID['Treasurer']: + if club and club.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]: return True return False def get_absolute_url(self): - return reverse('accounting:co_edit', kwargs={'co_id': self.id}) + return reverse("accounting:co_edit", kwargs={"co_id": self.id}) def get_display_name(self): return self.name @@ -105,14 +107,14 @@ class Company(models.Model): 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) + 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")) class Meta: verbose_name = _("Bank account") - ordering = ['club', 'name'] + ordering = ["club", "name"] def is_owned_by(self, user): """ @@ -121,25 +123,27 @@ class BankAccount(models.Model): if user.is_in_group(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): return True m = self.club.get_membership_for(user) - if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID['Treasurer']: + if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]: return True return False def get_absolute_url(self): - return reverse('accounting:bank_details', kwargs={'b_account_id': self.id}) + return reverse("accounting:bank_details", kwargs={"b_account_id": self.id}) def __str__(self): return self.name class ClubAccount(models.Model): - name = models.CharField(_('name'), max_length=30) + name = models.CharField(_("name"), max_length=30) club = models.ForeignKey(Club, related_name="club_account", verbose_name=_("club")) - bank_account = models.ForeignKey(BankAccount, related_name="club_accounts", verbose_name=_("bank account")) + bank_account = models.ForeignKey( + BankAccount, related_name="club_accounts", verbose_name=_("bank account") + ) class Meta: verbose_name = _("Club account") - ordering = ['bank_account', 'name'] + ordering = ["bank_account", "name"] def is_owned_by(self, user): """ @@ -154,7 +158,7 @@ class ClubAccount(models.Model): Method to see if that object can be edited by the given user """ m = self.club.get_membership_for(user) - if m and m.role == settings.SITH_CLUB_ROLES_ID['Treasurer']: + if m and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]: return True return False @@ -163,7 +167,7 @@ class ClubAccount(models.Model): Method to see if that object can be viewed by the given user """ m = self.club.get_membership_for(user) - if m and m.role >= settings.SITH_CLUB_ROLES_ID['Treasurer']: + if m and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]: return True return False @@ -177,30 +181,36 @@ class ClubAccount(models.Model): return self.journals.filter(closed=False).first() def get_absolute_url(self): - return reverse('accounting:club_details', kwargs={'c_account_id': self.id}) + return reverse("accounting:club_details", kwargs={"c_account_id": self.id}) def __str__(self): return self.name def get_display_name(self): - return _("%(club_account)s on %(bank_account)s") % {"club_account": self.name, "bank_account": self.bank_account} + 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")) - amount = CurrencyField(_('amount'), default=0) - effective_amount = CurrencyField(_('effective_amount'), default=0) + + 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") + ) + amount = CurrencyField(_("amount"), default=0) + effective_amount = CurrencyField(_("effective_amount"), default=0) class Meta: verbose_name = _("General journal") - ordering = ['-start_date'] + ordering = ["-start_date"] def is_owned_by(self, user): """ @@ -226,7 +236,7 @@ class GeneralJournal(models.Model): return self.club_account.can_be_viewed_by(user) def get_absolute_url(self): - return reverse('accounting:journal_details', kwargs={'j_id': self.id}) + return reverse("accounting:journal_details", kwargs={"j_id": self.id}) def __str__(self): return self.name @@ -250,31 +260,79 @@ 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")) - 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) - done = models.BooleanField(_('is done'), default=False) - simpleaccounting_type = models.ForeignKey('SimplifiedAccountingType', related_name="operations", - verbose_name=_("simple type"), null=True, blank=True) - accounting_type = models.ForeignKey('AccountingType', related_name="operations", - verbose_name=_("accounting type"), null=True, blank=True) - 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) + + number = models.IntegerField(_("number")) + journal = models.ForeignKey( + GeneralJournal, related_name="operations", null=False, verbose_name=_("journal") + ) + 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, + ) + done = models.BooleanField(_("is done"), default=False) + simpleaccounting_type = models.ForeignKey( + "SimplifiedAccountingType", + related_name="operations", + verbose_name=_("simple type"), + null=True, + blank=True, + ) + accounting_type = models.ForeignKey( + "AccountingType", + related_name="operations", + verbose_name=_("accounting type"), + null=True, + blank=True, + ) + 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, + ) class Meta: - unique_together = ('number', 'journal') - ordering = ['-number'] + unique_together = ("number", "journal") + ordering = ["-number"] def __getattribute__(self, attr): if attr == "target": @@ -287,14 +345,29 @@ class Operation(models.Model): 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)}) + 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")) + 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")) + 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 @@ -329,7 +402,7 @@ class Operation(models.Model): if self.journal.closed: return False m = self.journal.club_account.club.get_membership_for(user) - if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID['Treasurer']: + if m is not None and m.role >= settings.SITH_CLUB_ROLES_ID["Treasurer"]: return True return False @@ -342,16 +415,19 @@ class Operation(models.Model): if self.journal.closed: return False m = self.journal.club_account.club.get_membership_for(user) - if m is not None and m.role == settings.SITH_CLUB_ROLES_ID['Treasurer']: + if m is not None and m.role == settings.SITH_CLUB_ROLES_ID["Treasurer"]: return True return False def get_absolute_url(self): - return reverse('accounting:journal_details', kwargs={'j_id': self.journal.id}) + return reverse("accounting:journal_details", kwargs={"j_id": self.journal.id}) def __str__(self): return "%d € | %s | %s | %s" % ( - self.amount, self.date, self.accounting_type, self.done, + self.amount, + self.date, + self.accounting_type, + self.done, ) @@ -361,18 +437,30 @@ class AccountingType(models.Model): Thoses 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')), - ], + + 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, ) - 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'] + ordering = ["movement_type", "code"] def is_owned_by(self, user): """ @@ -383,7 +471,7 @@ class AccountingType(models.Model): return False def get_absolute_url(self): - return reverse('accounting:type_list') + return reverse("accounting:type_list") def __str__(self): return self.code + " - " + self.get_movement_type_display() + " - " + self.label @@ -393,13 +481,17 @@ class SimplifiedAccountingType(models.Model): """ Class describing the simplified accounting types. """ - label = models.CharField(_('label'), max_length=128) - accounting_type = models.ForeignKey(AccountingType, related_name="simplified_types", - verbose_name=_("simplified accounting types")) + + label = models.CharField(_("label"), max_length=128) + accounting_type = models.ForeignKey( + AccountingType, + related_name="simplified_types", + verbose_name=_("simplified accounting types"), + ) class Meta: verbose_name = _("simplified type") - ordering = ['accounting_type__movement_type', 'accounting_type__code'] + ordering = ["accounting_type__movement_type", "accounting_type__code"] @property def movement_type(self): @@ -409,25 +501,36 @@ class SimplifiedAccountingType(models.Model): return self.accounting_type.get_movement_type_display() def get_absolute_url(self): - return reverse('accounting:simple_type_list') + return reverse("accounting:simple_type_list") def __str__(self): - return self.get_movement_type_display() + " - " + self.accounting_type.code + " - " + self.label + return ( + self.get_movement_type_display() + + " - " + + self.accounting_type.code + + " - " + + self.label + ) 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")) + + name = models.CharField(_("label"), max_length=64) + club_account = models.ForeignKey( + ClubAccount, related_name="labels", verbose_name=_("club account") + ) class Meta: - unique_together = ('name', 'club_account') + 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}) + return reverse( + "accounting:label_list", kwargs={"clubaccount_id": self.club_account.id} + ) def is_owned_by(self, user): return self.club_account.is_owned_by(user) diff --git a/accounting/tests.py b/accounting/tests.py index ceb31c29..df4fd8d7 100644 --- a/accounting/tests.py +++ b/accounting/tests.py @@ -28,7 +28,13 @@ from django.core.management import call_command from datetime import date from core.models import User -from accounting.models import GeneralJournal, Operation, Label, AccountingType, SimplifiedAccountingType +from accounting.models import ( + GeneralJournal, + Operation, + Label, + AccountingType, + SimplifiedAccountingType, +) class RefoundAccountTest(TestCase): @@ -40,18 +46,20 @@ class RefoundAccountTest(TestCase): self.skia.customer.save() def test_permission_denied(self): - self.client.login(username='guy', password='plop') - response_post = self.client.post(reverse("accounting:refound_account"), - {"user": self.skia.id}) + self.client.login(username="guy", password="plop") + response_post = self.client.post( + reverse("accounting:refound_account"), {"user": self.skia.id} + ) response_get = self.client.get(reverse("accounting:refound_account")) self.assertTrue(response_get.status_code == 403) self.assertTrue(response_post.status_code == 403) def test_root_granteed(self): - self.client.login(username='root', password='plop') - response_post = self.client.post(reverse("accounting:refound_account"), - {"user": self.skia.id}) - self.skia = User.objects.filter(username='skia').first() + self.client.login(username="root", password="plop") + response_post = self.client.post( + reverse("accounting:refound_account"), {"user": self.skia.id} + ) + self.skia = User.objects.filter(username="skia").first() response_get = self.client.get(reverse("accounting:refound_account")) self.assertFalse(response_get.status_code == 403) self.assertTrue('