misc: add and apply pre-commit hooks (#58764)

This commit is contained in:
Benjamin Dauvergne 2021-11-19 13:55:48 +01:00
parent 63aafcdb1f
commit b4475d710f
47 changed files with 350 additions and 283 deletions

18
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,18 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/psf/black
rev: 20.8b1
hooks:
- id: black
args: ['--target-version', 'py37', '--skip-string-normalization', '--line-length', '110']
- repo: https://github.com/PyCQA/isort
rev: 5.7.0
hooks:
- id: isort
args: ['--profile', 'black', '--line-length', '110']
- repo: https://github.com/asottile/pyupgrade
rev: v2.20.0
hooks:
- id: pyupgrade
args: ['--keep-percent-format', '--py37-plus']

View File

@ -15,9 +15,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import csv import csv
from io import BytesIO
import os import os
import zipfile import zipfile
from io import BytesIO
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import HttpResponse from django.http import HttpResponse
@ -65,7 +65,7 @@ def export_references_as_fodt(modeladmin, request, queryset):
output_filename = 'references.fodt' output_filename = 'references.fodt'
mimetype = 'application/vnd.oasis.opendocument.text' mimetype = 'application/vnd.oasis.opendocument.text'
t = Template(open(template, 'r').read()) t = Template(open(template).read())
export = t.render(Context(context)).encode('utf-8') export = t.render(Context(context)).encode('utf-8')
response = HttpResponse(content=export, content_type=mimetype) response = HttpResponse(content=export, content_type=mimetype)

View File

@ -14,17 +14,16 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import taggit.admin
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.models import User, Group from django.contrib.auth.admin import GroupAdmin, UserAdmin
from django.contrib.auth.admin import UserAdmin, GroupAdmin from django.contrib.auth.models import Group, User
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.utils.http import urlencode from django.utils.http import urlencode
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
import taggit.admin
class EOGestionAdminSite(admin.AdminSite): class EOGestionAdminSite(admin.AdminSite):
@never_cache @never_cache
@ -32,18 +31,18 @@ class EOGestionAdminSite(admin.AdminSite):
if "mellon" in settings.INSTALLED_APPS: if "mellon" in settings.INSTALLED_APPS:
next_url = request.GET.get(REDIRECT_FIELD_NAME, settings.LOGIN_REDIRECT_URL or "/") next_url = request.GET.get(REDIRECT_FIELD_NAME, settings.LOGIN_REDIRECT_URL or "/")
query = urlencode({REDIRECT_FIELD_NAME: next_url}) query = urlencode({REDIRECT_FIELD_NAME: next_url})
url = "/accounts/mellon/login/?{0}".format(query) url = f"/accounts/mellon/login/?{query}"
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
return super(EOGestionAdminSite, self).login(request, extra_context=extra_context) return super().login(request, extra_context=extra_context)
@never_cache @never_cache
def logout(self, request, extra_context=None): def logout(self, request, extra_context=None):
if "mellon" in settings.INSTALLED_APPS: if "mellon" in settings.INSTALLED_APPS:
next_url = request.GET.get(REDIRECT_FIELD_NAME, settings.LOGIN_REDIRECT_URL or "/") next_url = request.GET.get(REDIRECT_FIELD_NAME, settings.LOGIN_REDIRECT_URL or "/")
query = urlencode({REDIRECT_FIELD_NAME: next_url}) query = urlencode({REDIRECT_FIELD_NAME: next_url})
url = "/accounts/mellon/logout/?{0}".format(query) url = f"/accounts/mellon/logout/?{query}"
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
return super(EOGestionAdminSite, self).logout(request, extra_context=extra_context) return super().logout(request, extra_context=extra_context)
site = EOGestionAdminSite() site = EOGestionAdminSite()

View File

@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.contrib import admin from django.contrib import admin
import eo_gestion.admin import eo_gestion.admin
from . import models from . import models

View File

@ -15,14 +15,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import itertools import itertools
import zipfile
from xml.dom import pulldom
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import zipfile
from django.core.files.storage import default_storage from xml.dom import pulldom
import requests import requests
from django.core.files.storage import default_storage
from . import chorus from . import chorus
@ -34,7 +32,7 @@ def grouper(iterable, n, fillvalue=None):
return itertools.zip_longest(*args, fillvalue=fillvalue) return itertools.zip_longest(*args, fillvalue=fillvalue)
class AnnuaireManager(object): class AnnuaireManager:
STRUCTURE_UNITAIRE_TAG_NAME = 'CPPStructurePartenaireUnitaire' STRUCTURE_UNITAIRE_TAG_NAME = 'CPPStructurePartenaireUnitaire'
def _update_annuaire(self): def _update_annuaire(self):
@ -48,7 +46,7 @@ class AnnuaireManager(object):
structures = [struct for struct in structures if struct] # ignore None structures = [struct for struct in structures if struct] # ignore None
inserts = [] inserts = []
updates = [] updates = []
identifiers = set(structure.full_identifier for structure in structures) identifiers = {structure.full_identifier for structure in structures}
known.update(identifiers) known.update(identifiers)
known_structures = { known_structures = {
struct.full_identifier: struct struct.full_identifier: struct

View File

@ -17,11 +17,10 @@
import base64 import base64
import logging import logging
import requests
from django.conf import settings from django.conf import settings
from django.utils.encoding import force_text from django.utils.encoding import force_text
import requests
from eo_gestion.decorators import cache from eo_gestion.decorators import cache
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -1,6 +1,7 @@
# Generated by Django 2.2.9 on 2020-02-04 09:08 # Generated by Django 2.2.9 on 2020-02-04 09:08
from django.db import migrations, models from django.db import migrations, models
import eo_gestion.chorus.validators import eo_gestion.chorus.validators
@ -31,7 +32,9 @@ class Migration(migrations.Migration):
('email', models.EmailField(max_length=254, null=True)), ('email', models.EmailField(max_length=254, null=True)),
('engagement_obligatoire', models.BooleanField()), ('engagement_obligatoire', models.BooleanField()),
], ],
options={'ordering': ('name', 'service_name'),}, options={
'ordering': ('name', 'service_name'),
},
), ),
migrations.CreateModel( migrations.CreateModel(
name='Update', name='Update',
@ -46,6 +49,7 @@ class Migration(migrations.Migration):
], ],
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='structure', index=models.Index(fields=['name', 'service_name'], name='name_idx'), model_name='structure',
index=models.Index(fields=['name', 'service_name'], name='name_idx'),
), ),
] ]

View File

@ -20,22 +20,30 @@ from .validators import validate_siret
class Update(models.Model): class Update(models.Model):
timestamp = models.DateTimeField(auto_now_add=True,) timestamp = models.DateTimeField(
success = models.BooleanField(default=False,) auto_now_add=True,
)
success = models.BooleanField(
default=False,
)
message = models.TextField() message = models.TextField()
class Structure(models.Model): class Structure(models.Model):
name = models.CharField(max_length=80) name = models.CharField(max_length=80)
full_identifier = models.CharField(max_length=len('29202001300010') + 64, unique=True) full_identifier = models.CharField(max_length=len('29202001300010') + 64, unique=True)
siret = models.CharField(max_length=len('29202001300010'), validators=[validate_siret], db_index=True,) siret = models.CharField(
max_length=len('29202001300010'),
validators=[validate_siret],
db_index=True,
)
service_code = models.CharField(max_length=64, default='', blank=True) service_code = models.CharField(max_length=64, default='', blank=True)
service_name = models.CharField(max_length=80, default='', blank=True) service_name = models.CharField(max_length=80, default='', blank=True)
email = models.EmailField(null=True) email = models.EmailField(null=True)
engagement_obligatoire = models.BooleanField() engagement_obligatoire = models.BooleanField()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Structure, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if not self.full_identifier: if not self.full_identifier:
self.full_identifier = (self.siret + self.service_code)[: len('29202001300010') + 64] self.full_identifier = (self.siret + self.service_code)[: len('29202001300010') + 64]

View File

@ -17,11 +17,12 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.contenttypes.admin import GenericStackedInline from django.contrib.contenttypes.admin import GenericStackedInline
from django.db.models import Sum, F, Q from django.db.models import F, Q, Sum
from .. import actions
from .. import admin as eo_gestion_admin
from ..eo_facture.admin import PaymentInline from ..eo_facture.admin import PaymentInline
from ..eo_facture.templatetags.eo_facture import amountformat from ..eo_facture.templatetags.eo_facture import amountformat
from .. import actions, admin as eo_gestion_admin
from . import models from . import models
@ -83,7 +84,7 @@ class LigneBanquePopAdmin(admin.ModelAdmin):
return True return True
def get_queryset(self, request): def get_queryset(self, request):
qs = super(LigneBanquePopAdmin, self).get_queryset(request) qs = super().get_queryset(request)
qs = qs.prefetch_related("payments") qs = qs.prefetch_related("payments")
return qs return qs

View File

@ -8,10 +8,10 @@ from eo_gestion.eo_banque.models import LigneBanquePop
class Command(BaseCommand): class Command(BaseCommand):
''' """
Charge un fichier CSV exporté depuis notre banque en ligne Banque Charge un fichier CSV exporté depuis notre banque en ligne Banque
Populaire Populaire
''' """
can_import_django_settings = True can_import_django_settings = True
output_transaction = True output_transaction = True
@ -39,7 +39,7 @@ class Command(BaseCommand):
def load_one_file(self, csv_file_path): def load_one_file(self, csv_file_path):
for delimiter in [";", "\t"]: for delimiter in [";", "\t"]:
csv_file = open(csv_file_path, 'r', encoding='latin1') csv_file = open(csv_file_path, encoding='latin1')
csv_lines = csv.reader(csv_file, delimiter=delimiter) csv_lines = csv.reader(csv_file, delimiter=delimiter)
first_line = next(csv_lines) first_line = next(csv_lines)
if first_line == self.HEADER: if first_line == self.HEADER:

View File

@ -7,9 +7,9 @@ from eo_gestion.eo_banque.models import SoldeBanquePop
class Command(BaseCommand): class Command(BaseCommand):
''' """
Charge un fichier CSV exporté depuis notre banque en ligne Baneque Populaire Charge un fichier CSV exporté depuis notre banque en ligne Baneque Populaire
''' """
can_import_django_settings = True can_import_django_settings = True
output_transaction = True output_transaction = True

View File

@ -1,4 +1,4 @@
from datetime import timedelta, date, datetime from datetime import date, datetime, timedelta
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db import transaction from django.db import transaction
@ -7,9 +7,9 @@ from eo_gestion.eo_banque.models import solde
class Command(BaseCommand): class Command(BaseCommand):
''' """
Charge un fichier CSV exporté depuis notre banque en ligne Banque Populaire Charge un fichier CSV exporté depuis notre banque en ligne Banque Populaire
''' """
can_import_django_settings = True can_import_django_settings = True
output_transaction = True output_transaction = True

View File

@ -1,4 +1,4 @@
from django.db import models, migrations from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -57,8 +57,8 @@ class Migration(migrations.Migration):
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='lignebanquepop', name='lignebanquepop',
unique_together=set( unique_together={
[('compte', 'date_comptabilisation', 'date_operation', 'libelle', 'reference', 'montant')] ('compte', 'date_comptabilisation', 'date_operation', 'libelle', 'reference', 'montant')
), },
), ),
] ]

View File

@ -16,9 +16,9 @@
from datetime import date from datetime import date
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils import six from django.utils import six
@ -44,7 +44,6 @@ def solde(t=None):
return m return m
@six.python_2_unicode_compatible
class Commentaire(models.Model): class Commentaire(models.Model):
contenu = models.TextField() contenu = models.TextField()
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
@ -56,11 +55,10 @@ class Commentaire(models.Model):
return "Commentaire créé le %s" % self.creation return "Commentaire créé le %s" % self.creation
@six.python_2_unicode_compatible
class LigneBanquePop(models.Model): class LigneBanquePop(models.Model):
''' """
Une ligne de notre relevé de compte Banque Populaire Une ligne de notre relevé de compte Banque Populaire
''' """
compte = models.CharField(max_length=20) compte = models.CharField(max_length=20)
date_comptabilisation = models.DateField() date_comptabilisation = models.DateField()
@ -79,13 +77,13 @@ class LigneBanquePop(models.Model):
return "%(date_valeur)s %(libelle)s %(montant)s" % self.__dict__ return "%(date_valeur)s %(libelle)s %(montant)s" % self.__dict__
def montant_non_affecte(self): def montant_non_affecte(self):
return self.montant - sum([x.montant_affecte for x in self.payments.all()]) return self.montant - sum(x.montant_affecte for x in self.payments.all())
class SoldeBanquePop(models.Model): class SoldeBanquePop(models.Model):
''' """
Le solde à un temps T, permet de calculer notre solde à toute date Le solde à un temps T, permet de calculer notre solde à toute date
''' """
compte = models.CharField(max_length=20) compte = models.CharField(max_length=20)
date = models.DateField(auto_now_add=True) date = models.DateField(auto_now_add=True)

View File

@ -21,8 +21,8 @@ from django import template
from django.db.models import Sum from django.db.models import Sum
from django.urls import reverse from django.urls import reverse
from ..models import LigneBanquePop, solde
from ...decorators import cache from ...decorators import cache
from ..models import LigneBanquePop, solde
register = template.Library() register = template.Library()
@ -84,8 +84,8 @@ def finances(month=3):
def total(context): def total(context):
qs = context["cl"].get_queryset(context["request"]) qs = context["cl"].get_queryset(context["request"])
ls = [ligne[0] for ligne in qs.values_list("montant")] ls = [ligne[0] for ligne in qs.values_list("montant")]
credit = sum([montant for montant in ls if montant > 0]) credit = sum(montant for montant in ls if montant > 0)
debit = sum([montant for montant in ls if montant < 0]) debit = sum(montant for montant in ls if montant < 0)
total = sum(ls) total = sum(ls)
return {'credit': credit, 'debit': debit, 'total': total} return {'credit': credit, 'debit': debit, 'total': total}

View File

@ -17,35 +17,35 @@
import datetime as dt import datetime as dt
from django.contrib import admin import django.http as http
from adminsortable2.admin import SortableInlineAdminMixin
from django.conf.urls import url from django.conf.urls import url
from django.contrib import admin
from django.contrib.admin.options import BaseModelAdmin
from django.contrib.humanize.templatetags.humanize import ordinal
from django.db.models import TextField
from django.forms import Textarea
from django.forms.models import BaseInlineFormSet
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse from django.urls import reverse
from django.contrib.admin.options import BaseModelAdmin
from django.db.models import TextField
import django.http as http
from django.contrib.humanize.templatetags.humanize import ordinal
from django.forms.models import BaseInlineFormSet
from django.forms import Textarea
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.six import BytesIO from django.utils.six import BytesIO
from adminsortable2.admin import SortableInlineAdminMixin
from .. import ods, actions
from . import forms, models, views
from .templatetags.eo_facture import amountformat
import eo_gestion.admin import eo_gestion.admin
from .. import actions, ods
from . import forms, models, views
from .templatetags.eo_facture import amountformat
class LookupAllowed(object):
class LookupAllowed:
def lookup_allowed(self, *args, **kwargs): def lookup_allowed(self, *args, **kwargs):
return True return True
class SelectRelatedMixin(object): class SelectRelatedMixin:
def get_queryset(self, request): def get_queryset(self, request):
qs = super(SelectRelatedMixin, self).get_queryset(request) qs = super().get_queryset(request)
return qs.select_related() return qs.select_related()
@ -55,7 +55,7 @@ class CommonPaymentInline(BaseModelAdmin):
kwargs['queryset'] = models.Facture.objects.avec_solde() kwargs['queryset'] = models.Facture.objects.avec_solde()
if db_field.name == 'ligne_banque_pop' and request.path.endswith('/add/'): if db_field.name == 'ligne_banque_pop' and request.path.endswith('/add/'):
kwargs['queryset'] = models.encaissements_avec_solde_non_affecte() kwargs['queryset'] = models.encaissements_avec_solde_non_affecte()
return super(CommonPaymentInline, self).formfield_for_foreignkey(db_field, request, **kwargs) return super().formfield_for_foreignkey(db_field, request, **kwargs)
class PrestationInline(SelectRelatedMixin, admin.TabularInline): class PrestationInline(SelectRelatedMixin, admin.TabularInline):
@ -70,7 +70,7 @@ class FactureInline(SelectRelatedMixin, admin.TabularInline):
class PaymentInlineFormset(BaseInlineFormSet): class PaymentInlineFormset(BaseInlineFormSet):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(PaymentInlineFormset, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for form in self.forms: for form in self.forms:
if form.instance.id is None: if form.instance.id is None:
field = form.fields["ligne_banque_pop"] field = form.fields["ligne_banque_pop"]
@ -135,7 +135,7 @@ class ClientAdmin(admin.ModelAdmin):
raw_id_fields = ['chorus_structure'] raw_id_fields = ['chorus_structure']
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
readonly_fields = super(ClientAdmin, self).get_readonly_fields(request, obj=obj) readonly_fields = super().get_readonly_fields(request, obj=obj)
if obj and obj.chorus_structure: if obj and obj.chorus_structure:
readonly_fields += ('siret', 'service_code') readonly_fields += ('siret', 'service_code')
return readonly_fields return readonly_fields
@ -186,7 +186,7 @@ class ContratAdmin(LookupAllowed, admin.ModelAdmin):
column_montant.short_description = 'Montant' column_montant.short_description = 'Montant'
def get_queryset(self, request): def get_queryset(self, request):
qs = super(ContratAdmin, self).get_queryset(request) qs = super().get_queryset(request)
return qs.prefetch_related('prestations', 'factures__lignes', 'tags') return qs.prefetch_related('prestations', 'factures__lignes', 'tags')
def tag_list(self, obj): def tag_list(self, obj):
@ -223,7 +223,7 @@ class ContratAdmin(LookupAllowed, admin.ModelAdmin):
return render(request, "admin/eo_facture/contrat/duplicate.html", context=context) return render(request, "admin/eo_facture/contrat/duplicate.html", context=context)
def get_urls(self): def get_urls(self):
urls = super(ContratAdmin, self).get_urls() urls = super().get_urls()
duplicate_view = self.admin_site.admin_view(self.duplicate) duplicate_view = self.admin_site.admin_view(self.duplicate)
my_urls = [url(r'^(.+)/duplicate/$', duplicate_view, name='eo_facture_contrat_duplicate')] my_urls = [url(r'^(.+)/duplicate/$', duplicate_view, name='eo_facture_contrat_duplicate')]
return my_urls + urls return my_urls + urls
@ -295,7 +295,7 @@ class FactureAdmin(LookupAllowed, admin.ModelAdmin):
def get_queryset(self, request): def get_queryset(self, request):
from django.db import connection from django.db import connection
qs = super(FactureAdmin, self).get_queryset(request) qs = super().get_queryset(request)
qs = qs.prefetch_related("lignes__facture__client") qs = qs.prefetch_related("lignes__facture__client")
qs = qs.prefetch_related("payments__ligne_banque_pop") qs = qs.prefetch_related("payments__ligne_banque_pop")
qs = qs.prefetch_related("contrat__factures") qs = qs.prefetch_related("contrat__factures")
@ -359,7 +359,7 @@ class FactureAdmin(LookupAllowed, admin.ModelAdmin):
return response return response
def get_urls(self): def get_urls(self):
urls = super(FactureAdmin, self).get_urls() urls = super().get_urls()
my_urls = [ my_urls = [
url(r'^view/([^/]*)', views.facture), url(r'^view/([^/]*)', views.facture),
url( url(
@ -378,7 +378,11 @@ class FactureAdmin(LookupAllowed, admin.ModelAdmin):
self.admin_site.admin_view(views.send_to_chorus), self.admin_site.admin_view(views.send_to_chorus),
name='eo_facture_facture_send_to_chorus', name='eo_facture_facture_send_to_chorus',
), ),
url(r"^sheet/$", self.admin_site.admin_view(self.sheet), name="eo_facture_facture_sheet",), url(
r"^sheet/$",
self.admin_site.admin_view(self.sheet),
name="eo_facture_facture_sheet",
),
] ]
return my_urls + urls return my_urls + urls

View File

@ -19,9 +19,8 @@ import os
import subprocess import subprocess
import tempfile import tempfile
from django.template.loader import render_to_string
import facturx import facturx
from django.template.loader import render_to_string
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -50,9 +49,7 @@ def to_pdfa(pdf_bytes: bytes, icc_profile: str = DEFAULT_ICC_PROFILE):
input_fd.name, input_fd.name,
] ]
logger.debug('converting to PDF/A, calling %s', args) logger.debug('converting to PDF/A, calling %s', args)
completed = subprocess.run( completed = subprocess.run(args, capture_output=True, text=True, check=False)
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, check=False
)
logger.debug('ghostscript result %s', completed) logger.debug('ghostscript result %s', completed)
if completed.returncode != 0: if completed.returncode != 0:
logger.error('ghostcript call failed %s', completed) logger.error('ghostcript call failed %s', completed)

View File

@ -17,17 +17,17 @@
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from django.core.exceptions import ValidationError
from django.core import validators
from django.db import models
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.core import validators
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.six import text_type from django.utils.six import text_type
from django.utils.translation import ugettext_lazy as _
class PercentagePerYear(list): class PercentagePerYear(list):
def __init__(self, sequence): def __init__(self, sequence):
super(PercentagePerYear, self).__init__(sequence) super().__init__(sequence)
def __str__(self): def __str__(self):
return ','.join(map(lambda p: ':'.join(map(str, [p[0], int(100 * p[1])])), self)) return ','.join(map(lambda p: ':'.join(map(str, [p[0], int(100 * p[1])])), self))
@ -37,7 +37,7 @@ class EuroField(models.DecimalField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs["max_digits"] = 8 kwargs["max_digits"] = 8
kwargs["decimal_places"] = 2 kwargs["decimal_places"] = 2
super(EuroField, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def assertion_error_to_validation_error(fun): def assertion_error_to_validation_error(fun):
@ -105,7 +105,7 @@ class PercentagePerYearField(models.Field):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs["max_length"] = 64 kwargs["max_length"] = 64
super(PercentagePerYearField, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def from_db_value(self, value, expression, connection, context): def from_db_value(self, value, expression, connection, context):
return self.to_python(value) return self.to_python(value)
@ -143,4 +143,4 @@ class PercentagePerYearField(models.Field):
def formfield(self, **kwargs): def formfield(self, **kwargs):
defaults = {"form_class": PercentagePerYearFormField} defaults = {"form_class": PercentagePerYearFormField}
defaults.update(kwargs) defaults.update(kwargs)
return super(PercentagePerYearField, self).formfield(**kwargs) return super().formfield(**kwargs)

View File

@ -19,9 +19,9 @@ import datetime
from decimal import Decimal from decimal import Decimal
from django import forms from django import forms
from django.db.transaction import atomic
from django.core.exceptions import ValidationError
from django.contrib.admin import widgets from django.contrib.admin import widgets
from django.core.exceptions import ValidationError
from django.db.transaction import atomic
from . import models from . import models
@ -37,7 +37,7 @@ class RapidFactureForm(forms.Form):
def __init__(self, request=None, *args, **kwargs): def __init__(self, request=None, *args, **kwargs):
self.request = request self.request = request
super(RapidFactureForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@atomic @atomic
def clean(self): def clean(self):
@ -89,7 +89,7 @@ class DuplicateContractForm(forms.Form):
def __init__(self, request=None, *args, **kwargs): def __init__(self, request=None, *args, **kwargs):
self.request = request self.request = request
super(DuplicateContractForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@atomic @atomic
def clean(self): def clean(self):

View File

@ -1,10 +1,12 @@
from django.db import models, migrations
import datetime import datetime
import eo_gestion.eo_facture.models
import eo_gestion.eo_facture.fields
from decimal import Decimal from decimal import Decimal
from django.conf import settings
import django.core.validators import django.core.validators
from django.conf import settings
from django.db import migrations, models
import eo_gestion.eo_facture.fields
import eo_gestion.eo_facture.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -46,11 +48,16 @@ class Migration(migrations.Migration):
( (
'tva', 'tva',
models.DecimalField( models.DecimalField(
default=Decimal('20'), verbose_name='TVA par défaut', max_digits=8, decimal_places=2, default=Decimal('20'),
verbose_name='TVA par défaut',
max_digits=8,
decimal_places=2,
), ),
), ),
], ],
options={'ordering': ('nom',),}, options={
'ordering': ('nom',),
},
bases=(models.Model,), bases=(models.Model,),
), ),
migrations.CreateModel( migrations.CreateModel(
@ -87,7 +94,9 @@ class Migration(migrations.Migration):
), ),
), ),
], ],
options={'ordering': ('-id',),}, options={
'ordering': ('-id',),
},
bases=(models.Model,), bases=(models.Model,),
), ),
migrations.CreateModel( migrations.CreateModel(
@ -168,7 +177,9 @@ class Migration(migrations.Migration):
), ),
), ),
], ],
options={'ordering': ('-id',),}, options={
'ordering': ('-id',),
},
bases=(models.Model,), bases=(models.Model,),
), ),
migrations.CreateModel( migrations.CreateModel(
@ -193,7 +204,9 @@ class Migration(migrations.Migration):
), ),
), ),
], ],
options={'ordering': ('order',),}, options={
'ordering': ('order',),
},
bases=(models.Model,), bases=(models.Model,),
), ),
migrations.CreateModel( migrations.CreateModel(
@ -230,7 +243,10 @@ class Migration(migrations.Migration):
), ),
), ),
], ],
options={'ordering': ('-ligne_banque_pop__date_valeur',), 'verbose_name': 'Paiement',}, options={
'ordering': ('-ligne_banque_pop__date_valeur',),
'verbose_name': 'Paiement',
},
bases=(models.Model,), bases=(models.Model,),
), ),
migrations.CreateModel( migrations.CreateModel(
@ -251,7 +267,9 @@ class Migration(migrations.Migration):
), ),
), ),
], ],
options={'ordering': ('contrat', 'id'),}, options={
'ordering': ('contrat', 'id'),
},
bases=(models.Model,), bases=(models.Model,),
), ),
] ]

View File

@ -1,4 +1,4 @@
from django.db import models, migrations from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -11,6 +11,8 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='client', name='contacts', field=models.TextField(blank=True, verbose_name='Contacts'), model_name='client',
name='contacts',
field=models.TextField(blank=True, verbose_name='Contacts'),
), ),
] ]

View File

@ -16,6 +16,7 @@
from decimal import Decimal from decimal import Decimal
from django.db import migrations, models from django.db import migrations, models

View File

@ -1,6 +1,7 @@
# Generated by Django 2.2.9 on 2020-02-01 14:59 # Generated by Django 2.2.9 on 2020-02-01 14:59
from django.db import migrations, models from django.db import migrations, models
import eo_gestion.eo_facture.validators import eo_gestion.eo_facture.validators

View File

@ -1,7 +1,7 @@
# Generated by Django 2.2.9 on 2020-02-01 15:00 # Generated by Django 2.2.9 on 2020-02-01 15:00
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -11,6 +11,8 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='contrat', name='url', field=models.URLField(blank=True, verbose_name='URL'), model_name='contrat',
name='url',
field=models.URLField(blank=True, verbose_name='URL'),
), ),
] ]

View File

@ -11,6 +11,8 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='client', name='active', field=models.BooleanField(default=True, verbose_name='Actif'), model_name='client',
name='active',
field=models.BooleanField(default=True, verbose_name='Actif'),
), ),
] ]

View File

@ -1,6 +1,7 @@
# Generated by Django 2.2.9 on 2020-07-04 14:55 # Generated by Django 2.2.9 on 2020-07-04 14:55
from django.db import migrations from django.db import migrations
import eo_gestion.eo_facture.taggit import eo_gestion.eo_facture.taggit

View File

@ -15,29 +15,28 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from decimal import Decimal, ROUND_HALF_UP
import datetime import datetime
from collections import defaultdict from collections import defaultdict
from decimal import ROUND_HALF_UP, Decimal
from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator, validate_email
from django.db import models from django.db import models
from django.db.models import Sum, Q, F from django.db.models import F, Q, Sum
from django.core.validators import validate_email, RegexValidator
from django.conf import settings
from django.db.models.signals import post_save, post_delete
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.db.models.signals import post_delete, post_save
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.translation import ugettext_lazy as _
from django.utils import six from django.utils import six
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from weasyprint import HTML from weasyprint import HTML
from eo_gestion.utils import percentage_str
from ..eo_banque import models as banque_models from ..eo_banque import models as banque_models
from . import fields, validators, facturx, taggit from . import facturx, fields, taggit, validators
from eo_gestion.utils import percentage_str
validate_telephone = RegexValidator(r"[. 0-9]*") validate_telephone = RegexValidator(r"[. 0-9]*")
@ -52,7 +51,6 @@ def today():
return now().date() return now().date()
@six.python_2_unicode_compatible
class Client(models.Model): class Client(models.Model):
nom = models.CharField(max_length=255, unique=True) nom = models.CharField(max_length=255, unique=True)
adresse = models.TextField() adresse = models.TextField()
@ -73,7 +71,12 @@ class Client(models.Model):
blank=True, blank=True,
default='', default='',
) )
service_code = models.CharField(max_length=128, db_index=True, blank=True, default='',) service_code = models.CharField(
max_length=128,
db_index=True,
blank=True,
default='',
)
active = models.BooleanField(verbose_name='Actif', default=True) active = models.BooleanField(verbose_name='Actif', default=True)
chorus_structure = models.ForeignKey( chorus_structure = models.ForeignKey(
@ -101,7 +104,6 @@ def one_hundred_percent_this_year():
return "%s:100" % now().date().year return "%s:100" % now().date().year
@six.python_2_unicode_compatible
class Contrat(models.Model): class Contrat(models.Model):
client = models.ForeignKey(Client, related_name="contrats", on_delete=models.CASCADE) client = models.ForeignKey(Client, related_name="contrats", on_delete=models.CASCADE)
intitule = models.CharField(max_length=150) intitule = models.CharField(max_length=150)
@ -123,7 +125,7 @@ class Contrat(models.Model):
tags = taggit.TaggableManager(blank=True) tags = taggit.TaggableManager(blank=True)
def montant_facture(self): def montant_facture(self):
return sum([facture.montant for facture in self.factures.non_proforma()]) return sum(facture.montant for facture in self.factures.non_proforma())
def pourcentage_facture(self): def pourcentage_facture(self):
return percentage_str(self.montant_facture(), self.montant()) return percentage_str(self.montant_facture(), self.montant())
@ -132,11 +134,11 @@ class Contrat(models.Model):
def montant(self): def montant(self):
""" """
Montant total d'un contrat, y compris les prestations Montant total d'un contrat, y compris les prestations
optionnelles. Si pas de prestation définie, montant des factures émises. optionnelles. Si pas de prestation définie, montant des factures émises.
""" """
if self.prestations.exists(): if self.prestations.exists():
return Decimal(sum([p.montant() for p in self.prestations.all()])) return Decimal(sum(p.montant() for p in self.prestations.all()))
else: else:
return self.montant_facture() return self.montant_facture()
@ -145,7 +147,7 @@ class Contrat(models.Model):
def nom_client(self): def nom_client(self):
""" """
Le nom du client qui a signé ce contrat avec EO. Le nom du client qui a signé ce contrat avec EO.
""" """
return self.client.nom return self.client.nom
@ -159,7 +161,6 @@ class Contrat(models.Model):
ordering = ('-id',) ordering = ('-id',)
@six.python_2_unicode_compatible
class Prestation(models.Model): class Prestation(models.Model):
contrat = models.ForeignKey(Contrat, related_name='prestations', on_delete=models.CASCADE) contrat = models.ForeignKey(Contrat, related_name='prestations', on_delete=models.CASCADE)
intitule = models.CharField(max_length=255, verbose_name='Intitulé') intitule = models.CharField(max_length=255, verbose_name='Intitulé')
@ -213,7 +214,6 @@ class FactureQuerySet(QuerySet):
) )
@six.python_2_unicode_compatible
class Facture(models.Model): class Facture(models.Model):
proforma = models.BooleanField(default=True, verbose_name='Facture proforma', db_index=True) proforma = models.BooleanField(default=True, verbose_name='Facture proforma', db_index=True)
ordre = models.IntegerField( ordre = models.IntegerField(
@ -274,7 +274,7 @@ class Facture(models.Model):
def save(self): def save(self):
if self.ordre is None and not self.proforma: if self.ordre is None and not self.proforma:
self.ordre = self.last_ordre_plus_one() self.ordre = self.last_ordre_plus_one()
super(Facture, self).save() super().save()
def clean(self): def clean(self):
if not (self.contrat or self.client): if not (self.contrat or self.client):
@ -333,7 +333,7 @@ class Facture(models.Model):
@property @property
def montant(self): def montant(self):
return sum([ligne.montant for ligne in self.lignes.all()]) return sum(ligne.montant for ligne in self.lignes.all())
def solde(self): def solde(self):
payments = self.payments.all() payments = self.payments.all()
@ -395,7 +395,6 @@ class Facture(models.Model):
ordering = ("-id",) ordering = ("-id",)
@six.python_2_unicode_compatible
class Ligne(models.Model): class Ligne(models.Model):
facture = models.ForeignKey(Facture, related_name="lignes", on_delete=models.CASCADE) facture = models.ForeignKey(Facture, related_name="lignes", on_delete=models.CASCADE)
intitule = models.TextField(blank=True) intitule = models.TextField(blank=True)
@ -428,8 +427,8 @@ class Ligne(models.Model):
if self.facture.contrat and not self.facture.proforma: if self.facture.contrat and not self.facture.proforma:
facture = self.facture facture = self.facture
contrat = facture.contrat contrat = facture.contrat
deja_facture = sum([f.montant for f in contrat.factures.exclude(id=facture.id)]) deja_facture = sum(f.montant for f in contrat.factures.exclude(id=facture.id))
deja_facture += sum([l.montant for l in facture.lignes.all() if l != self]) deja_facture += sum(l.montant for l in facture.lignes.all() if l != self)
if deja_facture + self.montant > contrat.montant(): if deja_facture + self.montant > contrat.montant():
errors.append( errors.append(
'Cette ligne fait dépasser le montant initial du contrat de %.2f %s' 'Cette ligne fait dépasser le montant initial du contrat de %.2f %s'
@ -442,7 +441,11 @@ class Ligne(models.Model):
ordering = ("order",) ordering = ("order",)
def __str__(self): def __str__(self):
return "%s pour %s %s" % (self.intitule, self.montant, self.facture.client.monnaie,) return "%s pour %s %s" % (
self.intitule,
self.montant,
self.facture.client.monnaie,
)
def encaissements_avec_solde_non_affecte(): def encaissements_avec_solde_non_affecte():
@ -455,7 +458,6 @@ def encaissements_avec_solde_non_affecte():
) )
@six.python_2_unicode_compatible
class Payment(models.Model): class Payment(models.Model):
facture = models.ForeignKey(Facture, related_name='payments', on_delete=models.CASCADE) facture = models.ForeignKey(Facture, related_name='payments', on_delete=models.CASCADE)
ligne_banque_pop = models.ForeignKey( ligne_banque_pop = models.ForeignKey(
@ -502,7 +504,10 @@ class Payment(models.Model):
pass pass
else: else:
deja_affecte = other_payments.aggregate(aggregate).get("montant_affecte") or 0 deja_affecte = other_payments.aggregate(aggregate).get("montant_affecte") or 0
if self.montant_affecte is not None and deja_affecte + self.montant_affecte > self.facture.montant_ttc: if (
self.montant_affecte is not None
and deja_affecte + self.montant_affecte > self.facture.montant_ttc
):
raise ValidationError( raise ValidationError(
'Le montant affecté aux différentes factures ' 'Le montant affecté aux différentes factures '
'est supérieur au montant de l\'encaissement.' 'est supérieur au montant de l\'encaissement.'

View File

@ -15,7 +15,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.forms import ModelMultipleChoiceField, SelectMultiple from django.forms import ModelMultipleChoiceField, SelectMultiple
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from taggit.models import Tag from taggit.models import Tag
@ -25,6 +24,7 @@ class TagWidget(SelectMultiple):
class Media: class Media:
'''Enable use of Select2 in template''' '''Enable use of Select2 in template'''
js = [ js = [
'xstatic/jquery.js', 'xstatic/jquery.js',
'xstatic/jquery-ui.js', 'xstatic/jquery-ui.js',
@ -42,9 +42,7 @@ class TagField(ModelMultipleChoiceField):
if value is None: if value is None:
return value return value
# TagManager returns Trough model and not the target model, we must adapt # TagManager returns Trough model and not the target model, we must adapt
if (hasattr(value, '__iter__') if hasattr(value, '__iter__') and not isinstance(value, str) and not hasattr(value, '_meta'):
and not isinstance(value, str)
and not hasattr(value, '_meta')):
if value and hasattr(value[0], '_meta'): if value and hasattr(value[0], '_meta'):
value = list(value.select_related('tag').values_list('tag__name', flat=True)) value = list(value.select_related('tag').values_list('tag__name', flat=True))
return value return value

View File

@ -15,21 +15,21 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from datetime import date, timedelta, datetime
from decimal import Decimal, InvalidOperation
from collections import defaultdict from collections import defaultdict
from datetime import date, datetime, timedelta
from decimal import Decimal, InvalidOperation
from django import template from django import template
from django.dispatch import receiver
from django.db import transaction from django.db import transaction
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_delete, post_save
from django.utils.formats import number_format from django.dispatch import receiver
from django.utils.timesince import timesince
from django.utils.six import text_type
from django.urls import reverse from django.urls import reverse
from django.utils.formats import number_format
from django.utils.six import text_type
from django.utils.timesince import timesince
from eo_gestion.eo_facture.models import Contrat, Facture, DELAI_PAIEMENT, Payment
from eo_gestion.eo_banque.models import LigneBanquePop from eo_gestion.eo_banque.models import LigneBanquePop
from eo_gestion.eo_facture.models import DELAI_PAIEMENT, Contrat, Facture, Payment
from eo_gestion.utils import percentage from eo_gestion.utils import percentage
from ...decorators import cache from ...decorators import cache
@ -107,7 +107,8 @@ def income():
invoiced_clients_by_year = {} invoiced_clients_by_year = {}
for year in invoiced_by_year_and_client: for year in invoiced_by_year_and_client:
clients = sorted( clients = sorted(
invoiced_by_year_and_client[year].keys(), key=lambda x: -invoiced_by_year_and_client[year][x], invoiced_by_year_and_client[year].keys(),
key=lambda x: -invoiced_by_year_and_client[year][x],
) )
invoiced_clients_by_year[year] = clients invoiced_clients_by_year[year] = clients
contracted_by_year = defaultdict(lambda: Decimal(0)) contracted_by_year = defaultdict(lambda: Decimal(0))
@ -153,7 +154,7 @@ class StringWithHref(text_type):
return text_type.__new__(cls, v) return text_type.__new__(cls, v)
def __init__(self, v, href=None): def __init__(self, v, href=None):
super(StringWithHref, self).__init__() super().__init__()
self.href = href self.href = href
@ -213,7 +214,7 @@ def income_by_clients(year=None):
total = sum(total_by_clients.values()) total = sum(total_by_clients.values())
total_invoiced = sum(invoiced_by_clients.values()) total_invoiced = sum(invoiced_by_clients.values())
total_contracted = sum(contracted_by_clients.values()) total_contracted = sum(contracted_by_clients.values())
percent_by_clients = dict([(i, Decimal(100) * v / total) for i, v in total_by_clients.items()]) percent_by_clients = {i: Decimal(100) * v / total for i, v in total_by_clients.items()}
clients = sorted(total_by_clients.keys(), key=lambda x: -total_by_clients[x]) clients = sorted(total_by_clients.keys(), key=lambda x: -total_by_clients[x])
running_total = Decimal(0) running_total = Decimal(0)
# compute pareto index # compute pareto index
@ -290,7 +291,7 @@ def a_facturer():
} }
) )
contrats_a_facturer.sort(key=lambda x: -x['depuis']) contrats_a_facturer.sort(key=lambda x: -x['depuis'])
montant = sum([x['montant'] for x in contrats_a_facturer]) montant = sum(x['montant'] for x in contrats_a_facturer)
return {'a_facturer': contrats_a_facturer, 'montant': montant} return {'a_facturer': contrats_a_facturer, 'montant': montant}

View File

@ -20,13 +20,14 @@ import logging
import os.path import os.path
from django import http from django import http
from django.shortcuts import get_object_or_404, redirect
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.admin.models import LogEntry from django.contrib.admin.models import LogEntry
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404, redirect
from eo_gestion.chorus.chorus import push_to_chorus from eo_gestion.chorus.chorus import push_to_chorus
from .models import Contrat, Facture from .models import Contrat, Facture
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -46,7 +47,8 @@ def facture_pdf(request, facture_id):
pdf = facture.pdf(base_uri=request.build_absolute_uri('/')) pdf = facture.pdf(base_uri=request.build_absolute_uri('/'))
if hasattr(settings, 'FACTURE_DIR'): if hasattr(settings, 'FACTURE_DIR'):
filename = os.path.join( filename = os.path.join(
settings.FACTURE_DIR, '%s-%s.pdf' % (facture.code(), facture.contrat.client.nom.encode('utf8')), settings.FACTURE_DIR,
'%s-%s.pdf' % (facture.code(), facture.contrat.client.nom.encode('utf8')),
) )
with open(filename, 'wb') as fd: with open(filename, 'wb') as fd:
fd.write(pdf) fd.write(pdf)
@ -90,11 +92,11 @@ def send_to_chorus(request, facture_id):
description = '' description = ''
if 'numeroFluxDepot' in response: if 'numeroFluxDepot' in response:
msg = 'Facture {facture} envoyée à ChorusPro'.format(facture=facture.code()) msg = f'Facture {facture.code()} envoyée à ChorusPro'
for key, value in response.items(): for key, value in response.items():
description += ' | {key} - {value}'.format(key=key, value=value) description += f' | {key} - {value}'
else: else:
msg = 'Échec d\'envoi de la facture {facture} à ChorusPro'.format(facture=facture.code()) msg = f'Échec d\'envoi de la facture {facture.code()} à ChorusPro'
msg += description msg += description
LogEntry.objects.create( LogEntry.objects.create(

View File

@ -18,10 +18,8 @@
import logging import logging
import django import django
from uwsgidecorators import timer from uwsgidecorators import timer
django.setup() django.setup()
logger = logging.getLogger('django.server') logger = logging.getLogger('django.server')

View File

@ -14,14 +14,13 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>. # along with this program; if not, see <http://www.gnu.org/licenses/>.
from datetime import date, datetime
import re import re
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import zipfile import zipfile
from datetime import date, datetime
from django.utils.encoding import force_text from django.utils.encoding import force_text
NS = { NS = {
"fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0", "fo": "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
"office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0", "office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
@ -46,7 +45,7 @@ def is_number(value):
return True return True
class Workbook(object): class Workbook:
def __init__(self): def __init__(self):
self.sheets = [] self.sheets = []
@ -158,7 +157,7 @@ class Workbook(object):
z.close() z.close()
class WorkSheet(object): class WorkSheet:
def __init__(self, workbook, name): def __init__(self, workbook, name):
self.cells = {} self.cells = {}
self.name = name self.name = name
@ -184,7 +183,7 @@ class WorkSheet(object):
return root return root
class WorkCell(object): class WorkCell:
def __init__(self, worksheet, value): def __init__(self, worksheet, value):
if value is None: if value is None:
value = "" value = ""

View File

@ -16,9 +16,8 @@
import os.path import os.path
from django.conf import global_settings
import facturx import facturx
from django.conf import global_settings
# Django settings for facturation project. # Django settings for facturation project.
@ -40,7 +39,12 @@ DATABASES = {
} }
} }
CACHES = {'default': {'BACKEND': 'django.core.cache.backends.db.DatabaseCache', 'LOCATION': 'cache',}} CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'cache',
}
}
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
@ -51,10 +55,23 @@ LOGGING = {
'datefmt': '%Y-%m-%d %a %H:%M:%S', 'datefmt': '%Y-%m-%d %a %H:%M:%S',
}, },
}, },
'handlers': {'console': {'level': 'DEBUG', 'class': 'logging.StreamHandler', 'formatter': 'verbose',}}, 'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'verbose',
}
},
'loggers': { 'loggers': {
'': {'handlers': ['console'], 'level': 'INFO',}, '': {
'factur-x': {'level': 'WARNING', 'handlers': [], 'propagate': False,}, 'handlers': ['console'],
'level': 'INFO',
},
'factur-x': {
'level': 'WARNING',
'handlers': [],
'propagate': False,
},
}, },
} }
@ -116,7 +133,9 @@ ROOT_URLCONF = "eo_gestion.urls"
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(os.path.dirname(__file__), 'templates'),], 'DIRS': [
os.path.join(os.path.dirname(__file__), 'templates'),
],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [

View File

@ -15,14 +15,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf.urls import include, url
from django.conf import settings
import django.contrib.admin import django.contrib.admin
from django.conf import settings
from django.conf.urls import include, url
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
from . import admin from . import admin
from .eo_facture.views import api_references from .eo_facture.views import api_references
django.contrib.admin.autodiscover() django.contrib.admin.autodiscover()

View File

@ -1,5 +1,5 @@
import os
import json import json
import os
from itertools import chain from itertools import chain
from types import MethodType from types import MethodType
@ -19,13 +19,10 @@ from django.db.models.functions import Coalesce
from django.db.models.signals import post_save, pre_save from django.db.models.signals import post_save, pre_save
from django.forms import widgets from django.forms import widgets
from django.forms.models import BaseInlineFormSet from django.forms.models import BaseInlineFormSet
from django.http import ( from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotAllowed
HttpResponse, HttpResponseBadRequest, from django.urls import path, reverse
HttpResponseNotAllowed, HttpResponseForbidden
)
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.urls import path, reverse
__all__ = ['SortableAdminMixin', 'SortableInlineAdminMixin'] __all__ = ['SortableAdminMixin', 'SortableInlineAdminMixin']
@ -55,12 +52,10 @@ class MovePageActionForm(admin.helpers.ActionForm):
required=False, required=False,
initial=1, initial=1,
widget=widgets.NumberInput(attrs={'id': 'changelist-form-step'}), widget=widgets.NumberInput(attrs={'id': 'changelist-form-step'}),
label=False label=False,
) )
page = forms.IntegerField( page = forms.IntegerField(
required=False, required=False, widget=widgets.NumberInput(attrs={'id': 'changelist-form-page'}), label=False
widget=widgets.NumberInput(attrs={'id': 'changelist-form-page'}),
label=False
) )
@ -91,7 +86,7 @@ class SortableAdminMixin(SortableAdminBase):
return [ return [
os.path.join('adminsortable2', app_label, opts.model_name, 'change_list.html'), os.path.join('adminsortable2', app_label, opts.model_name, 'change_list.html'),
os.path.join('adminsortable2', app_label, 'change_list.html'), os.path.join('adminsortable2', app_label, 'change_list.html'),
'adminsortable2/change_list.html' 'adminsortable2/change_list.html',
] ]
def __init__(self, model, admin_site): def __init__(self, model, admin_site):
@ -111,31 +106,22 @@ class SortableAdminMixin(SortableAdminBase):
# Insert the magic field into the same position as the first occurrence # Insert the magic field into the same position as the first occurrence
# of the default_order_field, or, if not present, at the start # of the default_order_field, or, if not present, at the start
try: try:
self.default_order_field_index = self.list_display.index( self.default_order_field_index = self.list_display.index(self.default_order_field)
self.default_order_field
)
except ValueError: except ValueError:
self.default_order_field_index = 0 self.default_order_field_index = 0
self.list_display.insert(self.default_order_field_index, '_reorder') self.list_display.insert(self.default_order_field_index, '_reorder')
# Remove *all* occurrences of the field from `list_display` # Remove *all* occurrences of the field from `list_display`
if self.list_display and self.default_order_field in self.list_display: if self.list_display and self.default_order_field in self.list_display:
self.list_display = [ self.list_display = [f for f in self.list_display if f != self.default_order_field]
f for f in self.list_display if f != self.default_order_field
]
# Remove *all* occurrences of the field from `list_display_links` # Remove *all* occurrences of the field from `list_display_links`
if self.list_display_links and self.default_order_field in self.list_display_links: if self.list_display_links and self.default_order_field in self.list_display_links:
self.list_display_links = [ self.list_display_links = [f for f in self.list_display_links if f != self.default_order_field]
f for f in self.list_display_links if
f != self.default_order_field
]
# Remove *all* occurrences of the field from `ordering` # Remove *all* occurrences of the field from `ordering`
if self.ordering and self.default_order_field in self.ordering: if self.ordering and self.default_order_field in self.ordering:
self.ordering = [ self.ordering = [f for f in self.ordering if f != self.default_order_field]
f for f in self.ordering if f != self.default_order_field
]
rev_field = '-' + self.default_order_field rev_field = '-' + self.default_order_field
if self.ordering and rev_field in self.ordering: if self.ordering and rev_field in self.ordering:
self.ordering = [f for f in self.ordering if f != rev_field] self.ordering = [f for f in self.ordering if f != rev_field]
@ -148,7 +134,7 @@ class SortableAdminMixin(SortableAdminBase):
path( path(
'adminsortable2_update/', 'adminsortable2_update/',
self.admin_site.admin_view(self.update_order), self.admin_site.admin_view(self.update_order),
name=self._get_update_url_name() name=self._get_update_url_name(),
), ),
] ]
return my_urls + super().get_urls() return my_urls + super().get_urls()
@ -209,10 +195,12 @@ class SortableAdminMixin(SortableAdminBase):
def media(self): def media(self):
m = super().media m = super().media
if self.enable_sorting: if self.enable_sorting:
m = m + widgets.Media(js=[ m = m + widgets.Media(
'adminsortable2/js/libs/jquery.ui.sortable-1.11.4.js', js=[
'adminsortable2/js/list-sortable.js', 'adminsortable2/js/libs/jquery.ui.sortable-1.11.4.js',
]) 'adminsortable2/js/list-sortable.js',
]
)
return m return m
def _add_reorder_method(self): def _add_reorder_method(self):
@ -223,11 +211,13 @@ class SortableAdminMixin(SortableAdminBase):
This can only be done using a function, since it is not possible This can only be done using a function, since it is not possible
to add dynamic attributes to bound methods. to add dynamic attributes to bound methods.
""" """
def func(this, item): def func(this, item):
html = '' html = ''
if this.enable_sorting: if this.enable_sorting:
html = '<div class="drag js-reorder-{1}" order="{0}">' \ html = '<div class="drag js-reorder-{1}" order="{0}">' '&nbsp;</div>'.format(
'&nbsp;</div>'.format(getattr(item, this.default_order_field), item.pk) getattr(item, this.default_order_field), item.pk
)
return mark_safe(html) return mark_safe(html)
setattr(func, 'allow_tags', True) setattr(func, 'allow_tags', True)
@ -255,36 +245,37 @@ class SortableAdminMixin(SortableAdminBase):
endorder = int(request.POST.get('endorder', 0)) endorder = int(request.POST.get('endorder', 0))
moved_items = list(self._move_item(request, startorder, endorder)) moved_items = list(self._move_item(request, startorder, endorder))
return HttpResponse( return HttpResponse(
json.dumps(moved_items, cls=DjangoJSONEncoder), json.dumps(moved_items, cls=DjangoJSONEncoder), content_type='application/json;charset=UTF-8'
content_type='application/json;charset=UTF-8'
) )
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
if not change: if not change:
setattr( setattr(obj, self.default_order_field, self.get_max_order(request, obj) + 1)
obj, self.default_order_field,
self.get_max_order(request, obj) + 1
)
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
def move_to_exact_page(self, request, queryset): def move_to_exact_page(self, request, queryset):
self._bulk_move(request, queryset, self.EXACT) self._bulk_move(request, queryset, self.EXACT)
move_to_exact_page.short_description = _('Move selected to specific page') move_to_exact_page.short_description = _('Move selected to specific page')
def move_to_back_page(self, request, queryset): def move_to_back_page(self, request, queryset):
self._bulk_move(request, queryset, self.BACK) self._bulk_move(request, queryset, self.BACK)
move_to_back_page.short_description = _('Move selected ... pages back') move_to_back_page.short_description = _('Move selected ... pages back')
def move_to_forward_page(self, request, queryset): def move_to_forward_page(self, request, queryset):
self._bulk_move(request, queryset, self.FORWARD) self._bulk_move(request, queryset, self.FORWARD)
move_to_forward_page.short_description = _('Move selected ... pages forward') move_to_forward_page.short_description = _('Move selected ... pages forward')
def move_to_first_page(self, request, queryset): def move_to_first_page(self, request, queryset):
self._bulk_move(request, queryset, self.FIRST) self._bulk_move(request, queryset, self.FIRST)
move_to_first_page.short_description = _('Move selected to first page') move_to_first_page.short_description = _('Move selected to first page')
def move_to_last_page(self, request, queryset): def move_to_last_page(self, request, queryset):
self._bulk_move(request, queryset, self.LAST) self._bulk_move(request, queryset, self.LAST)
move_to_last_page.short_description = _('Move selected to last page') move_to_last_page.short_description = _('Move selected to last page')
def _move_item(self, request, startorder, endorder): def _move_item(self, request, startorder, endorder):
@ -332,10 +323,7 @@ class SortableAdminMixin(SortableAdminBase):
move_qs = model.objects.filter(**move_filter).order_by(order_by) move_qs = model.objects.filter(**move_filter).order_by(order_by)
move_objs = list(move_qs) move_objs = list(move_qs)
for instance in move_objs: for instance in move_objs:
setattr( setattr(instance, rank_field, getattr(instance, rank_field) + move_delta)
instance, rank_field,
getattr(instance, rank_field) + move_delta
)
# Do not run `instance.save()`, because it will be updated # Do not run `instance.save()`, because it will be updated
# later in bulk by `move_qs.update`. # later in bulk by `move_qs.update`.
pre_save.send( pre_save.send(
@ -359,10 +347,10 @@ class SortableAdminMixin(SortableAdminBase):
setattr(obj, rank_field, endorder) setattr(obj, rank_field, endorder)
obj.save(update_fields=[rank_field]) obj.save(update_fields=[rank_field])
return [{ return [
'pk': instance.pk, {'pk': instance.pk, 'order': getattr(instance, rank_field)}
'order': getattr(instance, rank_field) for instance in chain(move_objs, [obj])
} for instance in chain(move_objs, [obj])] ]
@staticmethod @staticmethod
def get_extra_model_filters(request): def get_extra_model_filters(request):
@ -372,9 +360,7 @@ class SortableAdminMixin(SortableAdminBase):
return {} return {}
def get_max_order(self, request, obj=None): def get_max_order(self, request, obj=None):
return self.model.objects.aggregate( return self.model.objects.aggregate(max_order=Coalesce(Max(self.default_order_field), 0))['max_order']
max_order=Coalesce(Max(self.default_order_field), 0)
)['max_order']
def _bulk_move(self, request, queryset, method): def _bulk_move(self, request, queryset, method):
if not self.enable_sorting: if not self.enable_sorting:
@ -416,15 +402,9 @@ class SortableAdminMixin(SortableAdminBase):
self.message_user(request, msg, level=messages.ERROR) self.message_user(request, msg, level=messages.ERROR)
return return
endorders_start = getattr( endorders_start = getattr(objects[page.start_index() - 1], self.default_order_field)
objects[page.start_index() - 1], self.default_order_field
)
endorders_step = -1 if self.order_by.startswith('-') else 1 endorders_step = -1 if self.order_by.startswith('-') else 1
endorders = range( endorders = range(endorders_start, endorders_start + endorders_step * queryset_size, endorders_step)
endorders_start,
endorders_start + endorders_step * queryset_size,
endorders_step
)
if page.number > current_page_number: # Move forward (like drag down) if page.number > current_page_number: # Move forward (like drag down)
queryset = queryset.reverse() queryset = queryset.reverse()
@ -455,10 +435,11 @@ class PolymorphicSortableAdminMixin(SortableAdminMixin):
rather than ``admin.ModelAdmin``, then additionally inherit from ``PolymorphicSortableAdminMixin`` rather than ``admin.ModelAdmin``, then additionally inherit from ``PolymorphicSortableAdminMixin``
rather than ``SortableAdminMixin``. rather than ``SortableAdminMixin``.
""" """
def get_max_order(self, request, obj=None): def get_max_order(self, request, obj=None):
return self.base_model.objects.aggregate( return self.base_model.objects.aggregate(max_order=Coalesce(Max(self.default_order_field), 0))[
max_order=Coalesce(Max(self.default_order_field), 0) 'max_order'
)['max_order'] ]
class CustomInlineFormSetMixin: class CustomInlineFormSetMixin:
@ -466,8 +447,9 @@ class CustomInlineFormSetMixin:
self.default_order_direction, self.default_order_field = _get_default_ordering(self.model, self) self.default_order_direction, self.default_order_field = _get_default_ordering(self.model, self)
if self.default_order_field not in self.form.base_fields: if self.default_order_field not in self.form.base_fields:
self.form.base_fields[self.default_order_field] = \ self.form.base_fields[self.default_order_field] = self.model._meta.get_field(
self.model._meta.get_field(self.default_order_field).formfield() self.default_order_field
).formfield()
self.form.base_fields[self.default_order_field].is_hidden = True self.form.base_fields[self.default_order_field].is_hidden = True
self.form.base_fields[self.default_order_field].required = False self.form.base_fields[self.default_order_field].required = False
@ -476,12 +458,8 @@ class CustomInlineFormSetMixin:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def get_max_order(self): def get_max_order(self):
query_set = self.model.objects.filter( query_set = self.model.objects.filter(**{self.fk.get_attname(): self.instance.pk})
**{self.fk.get_attname(): self.instance.pk} return query_set.aggregate(max_order=Coalesce(Max(self.default_order_field), 0))['max_order']
)
return query_set.aggregate(
max_order=Coalesce(Max(self.default_order_field), 0)
)['max_order']
def save_new(self, form, commit=True): def save_new(self, form, commit=True):
""" """
@ -548,19 +526,18 @@ class SortableInlineAdminMixin(SortableAdminBase):
@property @property
def media(self): def media(self):
shared = ( shared = super().media + widgets.Media(
super().media + widgets.Media( js=('adminsortable2/js/libs/jquery.ui.sortable-1.11.4.js', 'adminsortable2/js/inline-sortable.js')
js=('adminsortable2/js/libs/jquery.ui.sortable-1.11.4.js', )
'adminsortable2/js/inline-sortable.js')))
if isinstance(self, admin.StackedInline): if isinstance(self, admin.StackedInline):
return shared + widgets.Media( return shared + widgets.Media(
js=('adminsortable2/js/inline-sortable.js', js=('adminsortable2/js/inline-sortable.js', 'adminsortable2/js/inline-stacked.js')
'adminsortable2/js/inline-stacked.js')) )
else: else:
# assume TabularInline (don't return None in any case) # assume TabularInline (don't return None in any case)
return shared + widgets.Media( return shared + widgets.Media(
js=('adminsortable2/js/inline-sortable.js', js=('adminsortable2/js/inline-sortable.js', 'adminsortable2/js/inline-tabular.js')
'adminsortable2/js/inline-tabular.js')) )
@property @property
def template(self): def template(self):
@ -580,14 +557,11 @@ class CustomGenericInlineFormSet(CustomInlineFormSetMixin, BaseGenericInlineForm
**{ **{
self.ct_fk_field.name: self.instance.pk, self.ct_fk_field.name: self.instance.pk,
self.ct_field.name: ContentType.objects.get_for_model( self.ct_field.name: ContentType.objects.get_for_model(
self.instance, self.instance, for_concrete_model=self.for_concrete_model
for_concrete_model=self.for_concrete_model ),
)
} }
) )
return query_set.aggregate( return query_set.aggregate(max_order=Coalesce(Max(self.default_order_field), 0))['max_order']
max_order=Coalesce(Max(self.default_order_field), 0)
)['max_order']
class SortableGenericInlineAdminMixin(SortableInlineAdminMixin): class SortableGenericInlineAdminMixin(SortableInlineAdminMixin):

View File

@ -4,4 +4,5 @@ import sys
sys.path.append(os.path.dirname(__file__)) sys.path.append(os.path.dirname(__file__))
os.environ['DJANGO_SETTINGS_MODULE'] = 'eo_gestion.settings' os.environ['DJANGO_SETTINGS_MODULE'] = 'eo_gestion.settings'
import django.core.handlers.wsgi import django.core.handlers.wsgi
application = django.core.handlers.wsgi.WSGIHandler() application = django.core.handlers.wsgi.WSGIHandler()

View File

@ -3,11 +3,11 @@
import os import os
import subprocess import subprocess
from distutils.cmd import Command from distutils.cmd import Command
from distutils.command.build import build as _build from distutils.command.build import build as _build
from distutils.command.sdist import sdist from distutils.command.sdist import sdist
from setuptools import setup, find_packages
from setuptools import find_packages, setup
from setuptools.command.install_lib import install_lib as _install_lib from setuptools.command.install_lib import install_lib as _install_lib
install_requires = [ install_requires = [
@ -23,11 +23,11 @@ install_requires = [
def get_version(): def get_version():
'''Use the VERSION, if absent generates a version with git describe, if not """Use the VERSION, if absent generates a version with git describe, if not
tag exists, take 0.0- and add the length of the commit log. tag exists, take 0.0- and add the length of the commit log.
''' """
if os.path.exists('VERSION'): if os.path.exists('VERSION'):
with open('VERSION', 'r') as v: with open('VERSION') as v:
return v.read() return v.read()
if os.path.exists('.git'): if os.path.exists('.git'):
p = subprocess.Popen( p = subprocess.Popen(

View File

@ -15,11 +15,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest import pytest
from django.core.management import call_command
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.management import call_command
DATA = ["tests/fixture.json"] DATA = ["tests/fixture.json"]

View File

@ -14,9 +14,10 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from eo_gestion.eo_facture.models import Contrat
from taggit.models import Tag from taggit.models import Tag
from eo_gestion.eo_facture.models import Contrat
def test_references(app): def test_references(app):
gru = Tag.objects.create(name='GRU') gru = Tag.objects.create(name='GRU')

View File

@ -14,9 +14,8 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest
import django_webtest import django_webtest
import pytest
@pytest.fixture @pytest.fixture

View File

@ -16,7 +16,6 @@
import os import os
ALLOWED_HOSTS = ["localhost"] ALLOWED_HOSTS = ["localhost"]
DATABASES = { DATABASES = {

View File

@ -14,9 +14,8 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest
import httmock import httmock
import pytest
import requests import requests
from eo_gestion.chorus import chorus from eo_gestion.chorus import chorus
@ -47,7 +46,12 @@ def chorus_connection_error(url, request):
@httmock.urlmatch() @httmock.urlmatch()
def chorus_error_500(url, request): def chorus_error_500(url, request):
return httmock.response( return httmock.response(
500, b'Pas content \xe9', headers={'Header Pourri': 'Héhé'.encode('latin1'), 'Header-Ok': 'ok',} 500,
b'Pas content \xe9',
headers={
'Header Pourri': 'Héhé'.encode('latin1'),
'Header-Ok': 'ok',
},
) )
@ -69,5 +73,8 @@ def test_push_to_chorus_error_500():
assert result == { assert result == {
'http.response.status-code': 500, 'http.response.status-code': 500,
'http.response.body': "b'Pas content \\xe9'", 'http.response.body': "b'Pas content \\xe9'",
'http.response.headers': {'Header Pourri': 'H<EFBFBD>h<EFBFBD>', 'Header-Ok': 'ok',}, 'http.response.headers': {
'Header Pourri': 'H<EFBFBD>h<EFBFBD>',
'Header-Ok': 'ok',
},
} }

View File

@ -16,12 +16,12 @@
import datetime import datetime
import io import io
import pytest
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import facturx import facturx
import pytest
from eo_gestion.eo_facture.facturx import to_pdfa, add_facturx_from_bytes from eo_gestion.eo_facture.facturx import add_facturx_from_bytes, to_pdfa
@pytest.fixture @pytest.fixture
@ -88,7 +88,14 @@ def test_add_facturx_from_bytes(fake_invoice_bytes):
['PostalTradeAddress', ['CountryID', 'FR']], ['PostalTradeAddress', ['CountryID', 'FR']],
['SpecifiedTaxRegistration', ['ID', 'FR09491081899']], ['SpecifiedTaxRegistration', ['ID', 'FR09491081899']],
], ],
['BuyerTradeParty', ['Name', 'RGFIPD'], ['SpecifiedLegalOrganization', ['ID', '1234'],],], [
'BuyerTradeParty',
['Name', 'RGFIPD'],
[
'SpecifiedLegalOrganization',
['ID', '1234'],
],
],
['BuyerOrderReferencedDocument', ['IssuerAssignedID', '5678']], ['BuyerOrderReferencedDocument', ['IssuerAssignedID', '5678']],
['ContractReferencedDocument', ['IssuerAssignedID', 'ABCD']], ['ContractReferencedDocument', ['IssuerAssignedID', 'ABCD']],
], ],

View File

@ -17,6 +17,7 @@
from datetime import date, timedelta from datetime import date, timedelta
from django.contrib.auth.models import User from django.contrib.auth.models import User
from eo_gestion.eo_facture.forms import FactureForm from eo_gestion.eo_facture.forms import FactureForm
from eo_gestion.eo_facture.models import Client, Contrat from eo_gestion.eo_facture.models import Client, Contrat
@ -86,4 +87,4 @@ def test_facture_form(db, freezer):
data['initial-emission'] = '2019-01-03' data['initial-emission'] = '2019-01-03'
form = FactureForm(data=dict(data, proforma='true', emission='2019-01-01'), instance=facture) form = FactureForm(data=dict(data, proforma='true', emission='2019-01-01'), instance=facture)
assert not form.is_valid(), form.errors assert not form.is_valid(), form.errors
assert set(form.errors) == set(['proforma', 'emission']) assert set(form.errors) == {'proforma', 'emission'}

View File

@ -1,6 +1,6 @@
[tox] [tox]
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/barbacompta/{env:BRANCH_NAME:} toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/barbacompta/{env:BRANCH_NAME:}
envlist = py3-pylint envlist = py3-pylint,code-style
[testenv] [testenv]
setenv = setenv =
@ -29,6 +29,13 @@ setenv =
commands = commands =
./manage.py {posargs:--help} ./manage.py {posargs:--help}
[testenv:code-style]
skip_install = true
deps =
pre-commit
commands =
pre-commit run --all-files --show-diff-on-failure
[pytest] [pytest]
filterwarnings= filterwarnings=
ignore:Using or importing the ABCs from ignore:Using or importing the ABCs from