migrate to python3 (#38507)

* add basic tests and big anonymized fixture
* add missing copyright notices
This commit is contained in:
Benjamin Dauvergne 2019-12-12 20:51:01 +01:00
parent 2c85151c22
commit e49e51a9e5
33 changed files with 840 additions and 397 deletions

View File

@ -1,6 +1,24 @@
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
import csv
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.utils.six import text_type
def export_as_csv(modeladmin, request, queryset):
@ -10,8 +28,8 @@ def export_as_csv(modeladmin, request, queryset):
if not request.user.is_staff:
raise PermissionDenied
opts = modeladmin.model._meta
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename=%s.csv' % unicode(opts).replace('.', '_')
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = "attachment; filename=%s.csv" % text_type(opts).replace(".", "_")
writer = csv.writer(response)
field_names = [field.name for field in opts.fields]
m2m_field_names = [m2m_field.name for m2m_field in opts.many_to_many]
@ -19,12 +37,12 @@ def export_as_csv(modeladmin, request, queryset):
writer.writerow(field_names + m2m_field_names)
# Write data rows
for obj in queryset:
values = [unicode(getattr(obj, field)) for field in field_names]
values = [text_type(getattr(obj, field)) for field in field_names]
for m2m_field in m2m_field_names:
value = getattr(obj, m2m_field)
value = u','.join(map(unicode, value.all()))
values.append(unicode(value))
writer.writerow(map(lambda x: unicode.encode(x, 'utf8'), values))
value = u",".join(map(text_type, value.all()))
values.append(text_type(value))
writer.writerow(map(lambda x: text_type.encode(x, "utf8"), values))
return response

View File

@ -1,8 +1,25 @@
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from django.conf import settings
from django.contrib import admin
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.models import *
from django.contrib.auth.admin import *
from django.contrib.auth.models import User, Group
from django.contrib.auth.admin import UserAdmin, GroupAdmin
from django.http import HttpResponseRedirect
from django.utils.http import urlencode
from django.views.decorators.cache import never_cache
@ -10,19 +27,19 @@ from django.views.decorators.cache import never_cache
class EOGestionAdminSite(admin.AdminSite):
@never_cache
def login(self, request, extra_context=None):
if settings.MELLON_IDENTITY_PROVIDERS:
next_url = request.GET.get(REDIRECT_FIELD_NAME, settings.LOGIN_REDIRECT_URL or '/')
if "mellon" in settings.INSTALLED_APPS:
next_url = request.GET.get(REDIRECT_FIELD_NAME, settings.LOGIN_REDIRECT_URL or "/")
query = urlencode({REDIRECT_FIELD_NAME: next_url})
url = '/accounts/mellon/login/?{0}'.format(query)
url = "/accounts/mellon/login/?{0}".format(query)
return HttpResponseRedirect(url)
return super(EOGestionAdminSite, self).login(request, extra_context=extra_context)
@never_cache
def logout(self, request, extra_context=None):
if settings.MELLON_IDENTITY_PROVIDERS:
next_url = request.GET.get(REDIRECT_FIELD_NAME, settings.LOGIN_REDIRECT_URL or '/')
if "mellon" in settings.INSTALLED_APPS:
next_url = request.GET.get(REDIRECT_FIELD_NAME, settings.LOGIN_REDIRECT_URL or "/")
query = urlencode({REDIRECT_FIELD_NAME: next_url})
url = '/accounts/mellon/logout/?{0}'.format(query)
url = "/accounts/mellon/logout/?{0}".format(query)
return HttpResponseRedirect(url)
return super(EOGestionAdminSite, self).logout(request, extra_context=extra_context)

View File

@ -1,13 +1,29 @@
# -*- coding: utf-8 -*-
# coding: utf-8
#
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from django.contrib import admin
from django.contrib.contenttypes.admin import GenericStackedInline
import models
import eo_gestion.admin
from .. import actions
from eo_gestion.eo_facture.admin import PaymentInline
from eo_gestion.eo_facture.templatetags.eo_facture import amountformat
from ..eo_facture.admin import PaymentInline
from ..eo_facture.templatetags.eo_facture import amountformat
from .. import actions, admin as eo_gestion_admin
from . import models
class CommentaireInlineAdmin(GenericStackedInline):
@ -49,13 +65,13 @@ class LigneBanquePopAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super(LigneBanquePopAdmin, self).get_queryset(request)
qs = qs.prefetch_related('payments')
qs = qs.prefetch_related("payments")
return qs
admin.site.register(models.LigneBanquePop, LigneBanquePopAdmin)
admin.site.register(models.SoldeBanquePop, list_display=['compte', 'date', 'montant'])
eo_gestion.admin.site.register(models.LigneBanquePop, LigneBanquePopAdmin)
eo_gestion.admin.site.register(models.SoldeBanquePop, list_display=['compte', 'date', 'montant'])
eo_gestion.admin.site.register(models.Commentaire)
eo_gestion_admin.site.register(models.LigneBanquePop, LigneBanquePopAdmin)
eo_gestion_admin.site.register(models.SoldeBanquePop, list_display=['compte', 'date', 'montant'])
eo_gestion_admin.site.register(models.Commentaire)

View File

@ -1,15 +1,13 @@
# -*- coding: utf-8 -*-
from optparse import make_option
import sys
import csv
from datetime import datetime, date
from __future__ import print_function, unicode_literals
# import xml.etree.ElementTree as etree
import csv
from datetime import datetime
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from eo_gestion.eo_banque.models import *
from eo_gestion.eo_banque.models import LigneBanquePop
class Command(BaseCommand):
@ -24,36 +22,36 @@ class Command(BaseCommand):
args = '<csv_file> <csv_file>...'
help = 'Charge les fichiers CSVs'
HEADER = [
u"Compte",
u"Date de comptabilisation",
u"Date opération",
u"Libellé",
u"Référence",
u"Date valeur",
u"Montant",
'Compte',
'Date de comptabilisation',
'Date opération',
'Libellé',
'Référence',
'Date valeur',
'Montant',
]
def add_arguments(self, parser):
parser.add_argument('args', nargs='+')
parser.add_argument("args", nargs="+")
def to_date(self, str):
try:
return datetime.strptime(str, '%Y/%m/%d').date()
except:
except Exception:
return datetime.strptime(str, '%d/%m/%Y').date()
def to_utf8(self, iter):
return map(lambda x: x.decode('latin1'), iter)
def load_one_file(self, csv_file_path):
for delimiter in [';', '\t']:
csv_file = file(csv_file_path)
for delimiter in [";", "\t"]:
csv_file = open(csv_file_path)
csv_lines = csv.reader(csv_file, delimiter=delimiter)
first_line = self.to_utf8(csv_lines.next())
if first_line == self.HEADER:
break
else:
raise CommandError('Invalid CSV file header')
raise CommandError("Invalid CSV file header")
counter = 0
loaded = 0
for line in csv_lines:

View File

@ -1,15 +1,11 @@
# -*- coding: utf-8 -*-
from optparse import make_option
import sys
import csv
from datetime import datetime, date
# coding: utf-8
# import xml.etree.ElementTree as etree
from datetime import date
from django.core.management.base import BaseCommand, CommandError
from django.core.management.base import BaseCommand
from django.db import transaction
from eo_gestion.eo_banque.models import *
from eo_gestion.eo_banque.models import SoldeBanquePop
class Command(BaseCommand):
@ -20,8 +16,8 @@ class Command(BaseCommand):
can_import_django_settings = True
output_transaction = True
requires_system_checks = True
args = '<csv_file> <csv_file>...'
help = 'Charge le solde courant'
args = "<csv_file> <csv_file>..."
help = "Charge le solde courant"
@transaction.atomic
def handle(self, compte, montant, **options):

View File

@ -1,17 +1,13 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
from optparse import make_option
import sys
import csv
from __future__ import print_function, unicode_literals
from datetime import timedelta, date, datetime
# import xml.etree.ElementTree as etree
from django.core.management.base import BaseCommand, CommandError
from django.core.management.base import BaseCommand
from django.db import transaction
from eo_gestion.eo_banque.models import *
from eo_gestion.eo_banque.models import solde
class Command(BaseCommand):

View File

@ -1,10 +1,27 @@
# -*- coding: utf-8 -*-
# coding: utf-8
#
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from datetime import date
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.utils import six
def solde(t=None):
@ -12,7 +29,7 @@ def solde(t=None):
if t is None:
t = date.today()
try:
s = SoldeBanquePop.objects.latest('date')
s = SoldeBanquePop.objects.latest("date")
except SoldeBanquePop.DoesNotExist:
return 0
m = s.montant
@ -29,17 +46,19 @@ def solde(t=None):
return m
@six.python_2_unicode_compatible
class Commentaire(models.Model):
contenu = models.TextField()
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
creation = models.DateTimeField(auto_now_add=True)
content_object = GenericForeignKey('content_type', 'object_id')
content_object = GenericForeignKey("content_type", "object_id")
def __unicode__(self):
return u'Commentaire créé le %s' % self.creation
def __str__(self):
return u"Commentaire créé le %s" % self.creation
@six.python_2_unicode_compatible
class LigneBanquePop(models.Model):
'''
Une ligne de notre relevé de compte Banque Populaire
@ -58,7 +77,7 @@ class LigneBanquePop(models.Model):
('compte', 'date_comptabilisation', 'date_operation', 'libelle', 'reference', 'montant'),
)
def __unicode__(self):
def __str__(self):
return "%(date_valeur)s %(libelle)s %(montant)s" % self.__dict__
def montant_non_affecte(self):

View File

@ -1,3 +1,19 @@
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from datetime import date, timedelta
from decimal import Decimal
@ -5,7 +21,8 @@ from django import template
from django.db.models import Sum
from django.core.urlresolvers import reverse
from eo_gestion.eo_banque.models import *
from ..models import LigneBanquePop, solde
from ...decorators import cache
register = template.Library()
@ -17,10 +34,10 @@ def month_before(date):
return date.replace(month=date.month - 1)
@register.inclusion_tag('eo_banque/finances.html')
@register.inclusion_tag("eo_banque/finances.html")
def finances(month=3):
dates = [date.today().replace(day=1)]
for i in xrange(1, month):
for i in range(1, month):
dates.append(month_before(dates[-1]))
e = []
lignes = LigneBanquePop.objects.filter(date_valeur__gte=min(dates), montant__gt=0)
@ -64,8 +81,8 @@ def finances(month=3):
@register.inclusion_tag('eo_banque/total.html', takes_context=True)
def total(context):
qs = context['cl'].get_queryset(context['request'])
ls = [ligne[0] for ligne in qs.values_list('montant')]
qs = context["cl"].get_queryset(context["request"])
ls = [ligne[0] for ligne in qs.values_list("montant")]
credit = sum([montant for montant in ls if montant > 0])
debit = sum([montant for montant in ls if montant < 0])
total = sum(ls)
@ -105,21 +122,22 @@ def week_start_and_end(year, month):
]
@register.inclusion_tag('admin/date_hierarchy.html')
@register.inclusion_tag("admin/date_hierarchy.html")
def eo_banque_date_hierarchy(cl):
if cl.date_hierarchy:
field_name = cl.date_hierarchy
year_field = '%s__year' % field_name
month_field = '%s__month' % field_name
day_field = '%s__day' % field_name
field_generic = '%s__' % field_name
field_gte = '%s__gte' % field_name
field_lte = '%s__lte' % field_name
year_field = "%s__year" % field_name
month_field = "%s__month" % field_name
day_field = "%s__day" % field_name
field_generic = "%s__" % field_name
field_gte = "%s__gte" % field_name
field_lte = "%s__lte" % field_name
year_lookup = cl.params.get(year_field)
month_lookup = cl.params.get(month_field)
day_lookup = cl.params.get(day_field)
link = lambda d: cl.get_query_string(d, [field_generic])
def link(d):
return cl.get_query_string(d, [field_generic])
if year_lookup and month_lookup and not day_lookup:
choices = []

View File

@ -1 +0,0 @@
# Create your views here.

View File

@ -1,15 +1,34 @@
# -*- coding: utf-8 -*-
# coding: utf-8
#
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from __future__ import unicode_literals
import datetime as dt
from django.contrib import admin
from django.conf.urls import url
from django.shortcuts import render
from django.template import RequestContext
from django.core.urlresolvers import reverse
from django.contrib.admin.options import BaseModelAdmin
import django.http as http
from django.contrib.humanize.templatetags.humanize import ordinal
from django.forms.models import BaseInlineFormSet
from django.utils.html import format_html
from . import forms, models, views
from .templatetags.eo_facture import amountformat
@ -51,11 +70,11 @@ class PaymentInlineFormset(BaseInlineFormSet):
super(PaymentInlineFormset, self).__init__(*args, **kwargs)
for form in self.forms:
if form.instance.id is None:
field = form.fields['ligne_banque_pop']
if not hasattr(field, 'parent_instance'):
field = form.fields["ligne_banque_pop"]
if not hasattr(field, "parent_instance"):
field.queryset = models.encaissements_avec_solde_non_affecte()
field = form.fields['facture']
if not hasattr(field, 'parent_instance'):
field = form.fields["facture"]
if not hasattr(field, "parent_instance"):
field.queryset = models.Facture.objects.avec_solde()
@ -73,7 +92,7 @@ class LigneInline(SelectRelatedMixin, admin.TabularInline):
model = models.Ligne
show_url = True
original = False
verbose_name_plural = u"Lignes de facture (vous pouvez les réordonner par drag&drop)"
verbose_name_plural = "Lignes de facture (vous pouvez les réordonner par drag&drop)"
extra = 0
@ -97,12 +116,11 @@ class ClientAdmin(admin.ModelAdmin):
def show_client(obj):
url = reverse('admin:eo_facture_client_change', args=[obj.client.id])
return u'<a href="%s">%s</a>' % (url, obj.client)
return format_html('<a href="{0}">{1}</a>', url, obj.client)
show_client.short_description = u'Client'
show_client.short_description = 'Client'
show_client.admin_order_field = 'client'
show_client.allow_tags = True
class ContratAdmin(LookupAllowed, admin.ModelAdmin):
@ -111,7 +129,7 @@ class ContratAdmin(LookupAllowed, admin.ModelAdmin):
PrestationInline,
]
list_display = [
'__unicode__',
"__str__",
show_client,
'column_montant',
'pourcentage_facture',
@ -132,8 +150,8 @@ class ContratAdmin(LookupAllowed, admin.ModelAdmin):
def get_queryset(self, request):
qs = super(ContratAdmin, self).get_queryset(request)
qs = qs.prefetch_related('prestations')
qs = qs.prefetch_related('factures__lignes')
qs = qs.prefetch_related("prestations")
qs = qs.prefetch_related("factures__lignes")
return qs
def save_model(self, request, obj, form, change):
@ -164,7 +182,7 @@ class ContratAdmin(LookupAllowed, admin.ModelAdmin):
'opts': self.model._meta,
}
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):
urls = super(ContratAdmin, self).get_urls()
@ -174,11 +192,8 @@ class ContratAdmin(LookupAllowed, admin.ModelAdmin):
def index(facture):
return ordinal(facture.index())
return format_html('{0}', ordinal(facture.index()))
index.short_description = "Ordre"
index.allow_tags = True
class FactureAdmin(LookupAllowed, admin.ModelAdmin):
@ -222,33 +237,30 @@ class FactureAdmin(LookupAllowed, admin.ModelAdmin):
def column_montant(self, obj):
return amountformat(obj.montant)
column_montant.short_description = 'Montant'
def column_montant_ttc(self, obj):
return amountformat(obj.montant_ttc)
column_montant_ttc.short_description = 'Montant TTC'
def column_solde(self, obj):
return amountformat(obj.solde())
column_solde.short_description = 'Solde'
def get_queryset(self, request):
from django.db import connection
qs = super(FactureAdmin, self).get_queryset(request)
qs = qs.prefetch_related('lignes__facture__client')
qs = qs.prefetch_related('payments__ligne_banque_pop')
qs = qs.prefetch_related('contrat__factures')
if connection.vendor == 'postgresql':
qs = qs.extra({'year': 'EXTRACT(year FROM emission)'})
elif connection.vendor == 'sqlite':
qs = qs.extra({'year': "strftime('%Y', emission)"})
qs = qs.prefetch_related("lignes__facture__client")
qs = qs.prefetch_related("payments__ligne_banque_pop")
qs = qs.prefetch_related("contrat__factures")
if connection.vendor == "postgresql":
qs = qs.extra({"year": "EXTRACT(year FROM emission)"})
elif connection.vendor == "sqlite":
qs = qs.extra({"year": "strftime('%Y', emission)"})
else:
qs = qs.extra({'year': 'YEAR(emission)'})
qs = qs.order_by('-year', '-proforma', '-ordre')
qs = qs.extra({"year": "YEAR(emission)"})
qs = qs.order_by("-year", "-proforma", "-ordre")
return qs
def save_model(self, request, obj, form, change):
@ -257,11 +269,11 @@ class FactureAdmin(LookupAllowed, admin.ModelAdmin):
obj.save()
class Media:
js = ('js/jquery.js', 'js/jquery-ui.js', 'js/menu-sort.js')
js = ("js/jquery.js", "js/jquery-ui.js", "js/menu-sort.js")
def add_simple(self, request):
context = {}
if request.method == 'POST':
if request.method == "POST":
form = forms.RapidFactureForm(request=request, data=request.POST)
if form.is_valid():
return http.HttpResponseRedirect(
@ -276,7 +288,7 @@ class FactureAdmin(LookupAllowed, admin.ModelAdmin):
'opts': self.model._meta,
}
return render(request, 'admin/eo_facture/facture/add_simple.html', context=context)
return render(request, "admin/eo_facture/facture/add_simple.html", context=context)
def get_urls(self):
urls = super(FactureAdmin, self).get_urls()
@ -298,19 +310,15 @@ class FactureAdmin(LookupAllowed, admin.ModelAdmin):
def show_client(self, obj):
url = reverse('admin:eo_facture_client_change', args=[obj.client.id])
return u'<a href="%s">%s</a>' % (url, obj.client)
return format_html('<a href="{0}">{1}</a>', url, obj.client)
show_client.short_description = u'Client'
show_client.allow_tags = True
def show_contrat(self, obj):
if obj.contrat:
url = reverse('admin:eo_facture_contrat_change', args=[obj.contrat.id])
return u'<a href="%s">%s</a>' % (url, obj.contrat)
return format_html('<a href="{0}">{1}</a>', url, obj.contrat)
return None
show_contrat.short_description = u'Contrat'
show_contrat.allow_tags = True
show_contrat.short_description = 'Contrat'
class PaymentAdmin(LookupAllowed, admin.ModelAdmin, CommonPaymentInline):

View File

@ -1,3 +1,23 @@
# coding: utf-8
#
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from __future__ import unicode_literals
from decimal import Decimal, InvalidOperation
from django.core.exceptions import ValidationError
@ -5,6 +25,8 @@ from django.core import validators
from django.db import models
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.utils.six import text_type
class PercentagePerYear(list):
@ -17,8 +39,8 @@ class PercentagePerYear(list):
class EuroField(models.DecimalField):
def __init__(self, *args, **kwargs):
kwargs['max_digits'] = 8
kwargs['decimal_places'] = 2
kwargs["max_digits"] = 8
kwargs["decimal_places"] = 2
super(EuroField, self).__init__(*args, **kwargs)
@ -26,7 +48,7 @@ def assertion_error_to_validation_error(fun):
def f(*args, **kwargs):
try:
return fun(*args, **kwargs)
except (AssertionError, ValueError), e:
except (AssertionError, ValueError) as e:
raise ValidationError(*e.args)
return f
@ -37,22 +59,22 @@ def check_percentage_per_year(value):
years = [a for a, b in value]
percentages = [b for a, b in value]
# ordered
assert years == sorted(years), 'years are not ordered'
assert years == sorted(years), "years are not ordered"
# sum equals 100
assert sum(percentages) == 1, 'percentage does not sum to 100'
assert sum(percentages) == 1, "percentage does not sum to 100"
# no duplicate year
assert len(years) == len(set(years)), 'years are not unique'
assert len(years) == len(set(years)), "years are not unique"
# consecutive
assert years == range(years[0], years[0] + len(years)), 'years are not consecutive'
def parse_percentage_per_year(value):
msg = _('field must be numeric values separated by commas')
values = value.split(',')
msg = _("field must be numeric values separated by commas")
values = value.split(",")
decimals = []
for value in values:
try:
year, decimal = value.split(':')
year, decimal = value.split(":")
except ValueError:
raise ValidationError(msg)
try:
@ -77,8 +99,8 @@ class PercentagePerYearFormField(forms.Field):
return None
if isinstance(PercentagePerYear, value):
return value
if not isinstance(unicode, value):
raise ValidationError(self.default_error_messages['invalid'])
if not isinstance(text_type, value):
raise ValidationError(self.default_error_messages["invalid"])
return parse_percentage_per_year(value)
@ -86,7 +108,7 @@ class PercentagePerYearField(models.Field):
default_validators = [check_percentage_per_year]
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 64
kwargs["max_length"] = 64
super(PercentagePerYearField, self).__init__(*args, **kwargs)
def from_db_value(self, value, expression, connection, context):
@ -97,12 +119,12 @@ class PercentagePerYearField(models.Field):
return value
if value is not None:
percentage_per_year = []
pairs = value.split(',')
pairs = value.split(",")
for pair in pairs:
try:
year, percentage = map(value.__class__.strip, pair.split(':'))
year, percentage = map(value.__class__.strip, pair.split(":"))
except ValueError:
raise ValidationError(PercentagePerYearFormField.default_error_messages['invalid'])
raise ValidationError(PercentagePerYearFormField.default_error_messages["invalid"])
year = int(year)
percentage = Decimal(percentage) / Decimal(100)
percentage_per_year.append((year, percentage))
@ -114,16 +136,16 @@ class PercentagePerYearField(models.Field):
def get_prep_value(self, value):
if isinstance(value, PercentagePerYear):
return unicode(value)
return text_type(value)
elif value is not None:
return unicode(parse_percentage_per_year(value))
return text_type(parse_percentage_per_year(value))
return value
def get_internal_type(self):
return 'CharField'
return "CharField"
def formfield(self, **kwargs):
defaults = {'form_class': PercentagePerYearFormField}
defaults = {"form_class": PercentagePerYearFormField}
defaults.update(kwargs)
return super(PercentagePerYearField, self).formfield(**kwargs)

View File

@ -1,5 +1,24 @@
# -*- encoding: utf-8 -*-
from decimal import *
# coding: utf-8
#
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from __future__ import unicode_literals
from decimal import Decimal
from django import forms
from django.db.transaction import atomic
@ -15,7 +34,7 @@ class RapidFactureForm(forms.Form):
contrat = forms.ModelChoiceField(queryset=models.Contrat.objects.all())
pourcentage = forms.ChoiceField(choices=pourcentages, initial='1')
solde = forms.BooleanField(
required=False, label=u'Ignorer le pourcentage, et solder le contrat en une ligne.'
required=False, label='Ignorer le pourcentage, et solder le contrat en une ligne.'
)
def __init__(self, request=None, *args, **kwargs):
@ -30,18 +49,18 @@ class RapidFactureForm(forms.Form):
facture = models.Facture(contrat=contrat, creator=self.request.user)
try:
facture.clean()
except ValidationError, e:
except ValidationError as e:
raise forms.ValidationError(*e.args)
facture.save()
self.cleaned_data['facture'] = facture
self.cleaned_data["facture"] = facture
lignes = []
errors = []
if solde:
montant_solde = contrat.solde()
if montant_solde == 0:
raise ValidationError(u'Le solde du contrat est déjà nul.')
raise ValidationError('Le solde du contrat est déjà nul.')
models.Ligne.objects.create(
facture=facture, intitule=u'Solde', prix_unitaire_ht=montant_solde, quantite=Decimal(1)
facture=facture, intitule='Solde', prix_unitaire_ht=montant_solde, quantite=Decimal(1)
)
else:
for prestation in contrat.prestations.all():
@ -54,9 +73,9 @@ class RapidFactureForm(forms.Form):
lignes.append(ligne)
try:
ligne.clean()
except ValidationError, e:
error = u'Il y a un problème avec la ligne « %s »: ' % prestation.intitule
error += '; '.join(map(lambda x: x.rstrip('.'), e.messages))
except ValidationError as e:
error = u"Il y a un problème avec la ligne « %s »: " % prestation.intitule
error += "; ".join(map(lambda x: x.rstrip("."), e.messages))
errors.append(error)
if errors:
raise forms.ValidationError(errors)
@ -68,7 +87,7 @@ class RapidFactureForm(forms.Form):
class DuplicateContractForm(forms.Form):
contrat = forms.ModelChoiceField(queryset=models.Contrat.objects.all())
new_intitule = forms.CharField(max_length=150, label=u'Nouvel intitulé')
new_intitule = forms.CharField(max_length=150, label=u"Nouvel intitulé")
def __init__(self, request=None, *args, **kwargs):
self.request = request
@ -87,10 +106,10 @@ class DuplicateContractForm(forms.Form):
)
try:
new_contrat.clean()
except ValidationError, e:
except ValidationError as e:
raise forms.ValidationError(*e.args)
new_contrat.save()
self.cleaned_data['new_contrat'] = new_contrat
self.cleaned_data["new_contrat"] = new_contrat
for prestation in contrat.prestations.all():
new_prestation = prestation.duplicate()
new_prestation.contrat = new_contrat
@ -102,31 +121,31 @@ class LigneForm(forms.ModelForm):
class Meta:
exclude = ()
model = models.Ligne
localized_fields = ('quantite', 'prix_unitaire_ht', 'taux_tva')
localized_fields = ("quantite", "prix_unitaire_ht", "taux_tva")
class PrestationForm(forms.ModelForm):
class Meta:
exclude = ()
model = models.Prestation
localized_fields = ('quantite', 'prix_unitaire_ht')
localized_fields = ("quantite", "prix_unitaire_ht")
class FactureForm(forms.ModelForm):
class Meta:
exclude = ()
model = models.Facture
localized_fields = ('taux_tva',)
localized_fields = ("taux_tva",)
class ClientForm(forms.ModelForm):
class Meta:
exclude = ()
model = models.Client
localized_fields = ('tva',)
localized_fields = ("tva",)
widgets = {
'adresse': widgets.AdminTextareaWidget(attrs={'rows': 4}),
'contacts': widgets.AdminTextareaWidget(attrs={'rows': 4}),
"adresse": widgets.AdminTextareaWidget(attrs={"rows": 4}),
"contacts": widgets.AdminTextareaWidget(attrs={"rows": 4}),
}
@ -134,11 +153,11 @@ class ContratForm(forms.ModelForm):
class Meta:
exclude = ()
model = models.Contrat
localized_fields = ('tva', 'montant_sous_traite')
localized_fields = ("tva", "montant_sous_traite")
class PaymentForm(forms.ModelForm):
class Meta:
exclude = ()
model = models.Payment
localized_fields = ('montant_affecte',)
localized_fields = ("montant_affecte",)

View File

@ -1,4 +1,21 @@
# -*- coding: utf-8 -*-
# coding: utf-8
#
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from __future__ import unicode_literals
from django.db import migrations, models

View File

@ -1,4 +1,21 @@
# -*- coding: utf-8 -*-
# coding: utf-8
#
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from __future__ import unicode_literals
from django.db import migrations, models

View File

@ -1,5 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.25 on 2019-10-09 11:35
# coding: utf-8
#
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from __future__ import unicode_literals
from decimal import Decimal

View File

@ -1,5 +1,24 @@
# -*- coding: utf-8 -*-
from decimal import *
# coding: utf-8
#
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from __future__ import unicode_literals
from decimal import Decimal, ROUND_HALF_UP
import datetime
from collections import defaultdict
@ -12,20 +31,22 @@ from django.conf import settings
from django.db.models.signals import post_save, post_delete
from django.db.models.query import QuerySet
from django.utils.translation import ugettext_lazy as _
from django.utils import six
from ..eo_banque import models as banque_models
import fields
from . import fields
from eo_gestion.utils import percentage_str
validate_telephone = RegexValidator(r'[. 0-9]*')
validate_telephone = RegexValidator(r"[. 0-9]*")
DEFAULT_TVA = getattr(settings, 'TVA', '20')
DEFAULT_TVA = getattr(settings, "TVA", "20")
def accounting_rounding(amount):
return amount.quantize(Decimal('0.01'), ROUND_HALF_UP)
return amount.quantize(Decimal("0.01"), ROUND_HALF_UP)
@six.python_2_unicode_compatible
class Client(models.Model):
nom = models.CharField(max_length=255, unique=True)
adresse = models.TextField()
@ -40,7 +61,7 @@ class Client(models.Model):
)
picture = models.ImageField('Logo', upload_to='logos/', blank=True, null=True)
def __unicode__(self):
def __str__(self):
return self.nom
class Meta:
@ -48,9 +69,10 @@ class Client(models.Model):
def one_hundred_percent_this_year():
return u'%s:100' % datetime.date.today().year
return u"%s:100" % datetime.date.today().year
@six.python_2_unicode_compatible
class Contrat(models.Model):
client = models.ForeignKey(Client, related_name="contrats")
intitule = models.CharField(max_length=150)
@ -72,35 +94,35 @@ class Contrat(models.Model):
def pourcentage_facture(self):
return percentage_str(self.montant_facture(), self.montant())
pourcentage_facture.short_description = u'Pourcentage facturé'
pourcentage_facture.short_description = 'Pourcentage facturé'
def montant(self):
'''
"""
Montant total d'un contrat, y compris les prestations
optionnelles.
'''
"""
return Decimal(sum([p.montant() for p in self.prestations.all()]))
def solde(self):
return self.montant() - self.montant_facture()
def nom_client(self):
'''
"""
Le nom du client qui a signé ce contrat avec EO.
'''
"""
return self.client.nom
def __unicode__(self):
def __str__(self):
return self.intitule
class Meta:
ordering = ('-id',)
@six.python_2_unicode_compatible
class Prestation(models.Model):
contrat = models.ForeignKey(Contrat, related_name='prestations')
intitule = models.CharField(max_length=255, verbose_name=u'Intitulé')
intitule = models.CharField(max_length=255, verbose_name='Intitulé')
optionnel = models.BooleanField(blank=True, default=False)
prix_unitaire_ht = models.DecimalField(max_digits=8, decimal_places=2)
quantite = models.DecimalField(max_digits=8, decimal_places=3)
@ -117,8 +139,8 @@ class Prestation(models.Model):
quantite=self.quantite,
)
def __unicode__(self):
return u'%s pour %5.2f € HT' % (
def __str__(self):
return '%s pour %5.2f € HT' % (
self.intitule,
accounting_rounding(self.prix_unitaire_ht * self.quantite),
)
@ -127,14 +149,14 @@ class Prestation(models.Model):
ordering = ('contrat', 'intitule')
DELAI_PAIEMENT = getattr(settings, 'DELAI_PAIEMENT', 45)
DELAI_PAIEMENT = getattr(settings, "DELAI_PAIEMENT", 45)
def today_plus_delai():
return datetime.date.today() + datetime.timedelta(days=DELAI_PAIEMENT)
echeance_verbose_name = u"Échéance (par défaut %d jours)" % getattr(settings, 'DELAI_PAIEMENT', 45)
echeance_verbose_name = 'Échéance (par défaut %d jours)' % getattr(settings, 'DELAI_PAIEMENT', 45)
class FactureQuerySet(QuerySet):
@ -151,6 +173,7 @@ class FactureQuerySet(QuerySet):
)
@six.python_2_unicode_compatible
class Facture(models.Model):
proforma = models.BooleanField(default=True, verbose_name='Facture proforma', db_index=True)
ordre = models.IntegerField(
@ -191,9 +214,9 @@ class Facture(models.Model):
return 'Facture proforma du %s' % self.emission
ctx = {'year': self.emission.year}
ctx.update(self.__dict__)
if ctx['ordre'] is None:
return 'Ordre is missing'
format = getattr(settings, 'FACTURE_CODE_FORMAT', u'F%(year)s%(ordre)04d')
if ctx["ordre"] is None:
return "Ordre is missing"
format = getattr(settings, "FACTURE_CODE_FORMAT", u"F%(year)s%(ordre)04d")
return format % ctx
def save(self):
@ -207,18 +230,18 @@ class Facture(models.Model):
self.intitule = self.contrat.intitule
if self.client:
if self.client != self.contrat.client:
raise ValidationError(u'Le client de la facture et du contrat doivent être identiques.')
raise ValidationError(u"Le client de la facture et du contrat doivent être identiques.")
else:
self.client = self.contrat.client
else:
if not self.intitule:
raise ValidationError(u'La facture doit avoir un intitulé')
raise ValidationError(u"La facture doit avoir un intitulé")
if not self.proforma:
try:
for ligne in self.lignes.all():
ligne.clean()
except ValidationError:
raise ValidationError(u'Il y a un problème avec les lignes de cette facture')
raise ValidationError(u"Il y a un problème avec les lignes de cette facture")
self.update_paid(save=False)
def index(self):
@ -227,7 +250,7 @@ class Facture(models.Model):
else:
return 1
def __unicode__(self):
def __str__(self):
return self.code()
@property
@ -236,8 +259,8 @@ class Facture(models.Model):
for ligne in self.lignes.all():
amount_by_vat[ligne.tva] += ligne.montant
vat = Decimal(0)
for vat_percentage, amount in amount_by_vat.iteritems():
var_ratio = vat_percentage / Decimal('100')
for vat_percentage, amount in amount_by_vat.items():
var_ratio = vat_percentage / Decimal("100")
vat += accounting_rounding(var_ratio * amount)
return vat
@ -271,11 +294,12 @@ class Facture(models.Model):
return self.emission.year
class Meta:
ordering = ('-id',)
ordering = ("-id",)
@six.python_2_unicode_compatible
class Ligne(models.Model):
facture = models.ForeignKey(Facture, related_name='lignes')
facture = models.ForeignKey(Facture, related_name="lignes")
intitule = models.TextField(blank=True)
prix_unitaire_ht = models.DecimalField(max_digits=8, decimal_places=2, default=Decimal('0'))
quantite = models.DecimalField(max_digits=8, decimal_places=3, default=Decimal('1.0'))
@ -300,9 +324,9 @@ class Ligne(models.Model):
def clean(self):
errors = []
if self.taux_tva and self.taux_tva < 0:
errors.append(u'Le taux de tva doit être une valeur positive ou nulle.')
errors.append(u"Le taux de tva doit être une valeur positive ou nulle.")
if self.prix_unitaire_ht < 0:
errors.append(u'Le prix unitaire hors taxe doit être une valeur positive ou nulle.')
errors.append(u"Le prix unitaire hors taxe doit être une valeur positive ou nulle.")
if self.facture.contrat and not self.facture.proforma:
facture = self.facture
contrat = facture.contrat
@ -310,17 +334,17 @@ class Ligne(models.Model):
deja_facture += sum([l.montant for l in facture.lignes.all() if l != self])
if deja_facture + self.montant > contrat.montant:
errors.append(
u'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'
% (deja_facture + self.montant - contrat.montant, contrat.client.monnaie)
)
if errors:
raise ValidationError(errors)
class Meta:
ordering = ('order',)
ordering = ("order",)
def __unicode__(self):
return '%s pour %s %s' % (self.intitule, self.montant, self.facture.client.monnaie)
def __str__(self):
return "%s pour %s %s" % (self.intitule, self.montant, self.facture.client.monnaie,)
def encaissements_avec_solde_non_affecte():
@ -333,43 +357,44 @@ def encaissements_avec_solde_non_affecte():
)
@six.python_2_unicode_compatible
class Payment(models.Model):
facture = models.ForeignKey(Facture, related_name='payments')
ligne_banque_pop = models.ForeignKey(
banque_models.LigneBanquePop,
related_name='payments',
verbose_name=u"Encaissement",
verbose_name='Encaissement',
limit_choices_to={'montant__gt': 0},
)
montant_affecte = models.DecimalField(
max_digits=8,
decimal_places=2,
blank=True,
verbose_name=u"Montant affecté",
help_text=u"Si vide, le montant non affecté de l'encaissement est pris comme valeur",
verbose_name='Montant affecté',
help_text="Si vide, le montant non affecté de l'encaissement est pris comme valeur",
)
def clean(self):
'''Vérifie la cohérence des paiements'''
"""Vérifie la cohérence des paiements"""
try:
if self.montant_affecte is None:
self.montant_affecte = min(self.ligne_banque_pop.montant, self.facture.montant_ttc)
except (banque_models.LigneBanquePop.DoesNotExist, Facture.DoesNotExist):
pass
aggregate = models.Sum('montant_affecte')
aggregate = models.Sum("montant_affecte")
# from the ligne de banque pov
try:
other_payments = self.ligne_banque_pop.payments
if self.ligne_banque_pop.montant < 0 or (self.montant_affecte and self.montant_affecte <= 0):
raise ValidationError('Un paiement ne peut être que d\'un montant positif')
raise ValidationError("Un paiement ne peut être que d'un montant positif")
except banque_models.LigneBanquePop.DoesNotExist:
pass
else:
deja_affecte = other_payments.aggregate(aggregate).get('montant_affecte', 0)
deja_affecte = other_payments.aggregate(aggregate).get("montant_affecte", 0)
if deja_affecte + self.montant_affecte > self.ligne_banque_pop.montant:
raise ValidationError(
u'Le montant affecté aux différentes factures '
u'est supérieur au montant de l\'encaissement.'
'Le montant affecté aux différentes factures '
'est supérieur au montant de l\'encaissement.'
)
# from the facture pov
try:
@ -377,19 +402,18 @@ class Payment(models.Model):
except Facture.DoesNotExist:
pass
else:
deja_affecte = other_payments.aggregate(aggregate).get('montant_affecte', 0)
deja_affecte = other_payments.aggregate(aggregate).get("montant_affecte", 0)
if deja_affecte + self.montant_affecte > self.facture.montant_ttc:
raise ValidationError(
u'Le montant affecté aux différentes factures '
u'est supérieur au montant de l\'encaissement.'
'Le montant affecté aux différentes factures '
'est supérieur au montant de l\'encaissement.'
)
def __unicode__(self):
return u'Paiement de %.2f € sur facture %s à %s' % (
def __str__(self):
return 'Paiement de %.2f € sur facture %s à %s' % (
self.ligne_banque_pop.montant,
unicode(self.facture),
unicode(self.facture.client),
self.facture,
self.facture.client,
)
class Meta:
@ -397,7 +421,9 @@ class Payment(models.Model):
ordering = ('-ligne_banque_pop__date_valeur',)
def update_paid(sender, instance, **kwargs):
def update_paid(sender, instance, raw=False, **kwargs):
if raw:
return
instance.facture.update_paid()

View File

@ -1,4 +1,23 @@
# -*- encoding: utf-8 -*-
# coding: utf-8
#
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from __future__ import unicode_literals
from datetime import date, timedelta, datetime
from decimal import Decimal, InvalidOperation
from collections import defaultdict
@ -6,12 +25,15 @@ from collections import defaultdict
from django import template
from django.utils.formats import number_format
from django.utils.timesince import timesince
from django.utils.six import text_type
from django.core.urlresolvers import reverse
from eo_gestion.eo_facture.models import Contrat, Facture, DELAI_PAIEMENT
from eo_gestion.eo_banque.models import LigneBanquePop
from eo_gestion.utils import percentage
from ...decorators import cache
register = template.Library()
@ -82,8 +104,9 @@ def income():
invoiced_by_year_and_client[invoice.accounting_year][invoice.client.id] += montant
invoiced_clients_by_year = {}
for year in invoiced_by_year_and_client:
clients = invoiced_by_year_and_client[year].keys()
clients.sort(key=lambda x: -invoiced_by_year_and_client[year][x])
clients = sorted(
invoiced_by_year_and_client[year].keys(), key=lambda x: -invoiced_by_year_and_client[year][x],
)
invoiced_clients_by_year[year] = clients
contracted_by_year = defaultdict(lambda: Decimal(0))
for contract in Contrat.objects.all().prefetch_related('factures', 'factures__lignes', 'prestations'):
@ -123,18 +146,18 @@ def zero_dict():
return defaultdict(lambda: Decimal(0))
class StringWithHref(unicode):
class StringWithHref(text_type):
def __new__(cls, v, href=None):
return unicode.__new__(cls, v)
return text_type.__new__(cls, v)
def __init__(self, v, href=None):
super(StringWithHref, self).__init__(v)
super(StringWithHref, self).__init__()
self.href = href
def client_and_link(c):
s = unicode(c)
url = reverse('admin:eo_facture_client_change', args=(c.id,))
s = text_type(c)
url = reverse("admin:eo_facture_client_change", args=(c.id,))
return StringWithHref(s, url)
@ -142,7 +165,7 @@ def dict_of_list():
return defaultdict(lambda: [])
@register.inclusion_tag('eo_facture/table.html')
@register.inclusion_tag("eo_facture/table.html")
def income_by_clients(year=None):
if not year:
year = date.today().year
@ -187,9 +210,8 @@ def income_by_clients(year=None):
total = sum(total_by_clients.values())
total_invoiced = sum(invoiced_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.iteritems()])
clients = total_by_clients.keys()
clients.sort(key=lambda x: -total_by_clients[x])
percent_by_clients = dict([(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])
running_total = Decimal(0)
# compute pareto index
pareto = dict()
@ -201,16 +223,16 @@ def income_by_clients(year=None):
name="previsional-income-by-client-%s" % year,
headers=[
('client', 'Client'),
('income', "Chiffre d'affaire"),
('invoiced', u"Facturé"),
('percent_invoiced', u"Pourcentage facturé"),
('contracted', u"Contracté"),
('percent_contracted', u"Pourcentage contracté"),
('income', 'Chiffre d\'affaire'),
('invoiced', 'Facturé'),
('percent_invoiced', 'Pourcentage facturé'),
('contracted', 'Contracté'),
('percent_contracted', 'Pourcentage contracté'),
('percent', 'Pourcentage'),
('pareto', 'Pareto'),
],
contracts_by_clients=contracts_by_clients.iteritems(),
invoices_by_clients=invoices_by_clients.iteritems(),
contracts_by_clients=list(contracts_by_clients.items()),
invoices_by_clients=list(invoices_by_clients.items()),
table=[
(
client_and_link(c),
@ -241,7 +263,7 @@ def income_by_clients(year=None):
@register.inclusion_tag('eo_facture/a_facturer.html')
def a_facturer():
l = []
contrats_a_facturer = []
for contrat in (
Contrat.objects.all()
.select_related('client')
@ -255,7 +277,7 @@ def a_facturer():
else:
depuis = contrat.creation
depuis = (date.today() - depuis).days
l.append(
contrats_a_facturer.append(
{
'contrat': contrat,
'pourcentage': (facture / a_facture) * Decimal(100),
@ -263,9 +285,9 @@ def a_facturer():
'depuis': depuis,
}
)
l.sort(key=lambda x: -x['depuis'])
montant = sum([x['montant'] for x in l])
return {'a_facturer': l, 'montant': montant}
contrats_a_facturer.sort(key=lambda x: -x['depuis'])
montant = sum([x['montant'] for x in contrats_a_facturer])
return {'a_facturer': contrats_a_facturer, 'montant': montant}
@register.filter(name='ago')
@ -277,4 +299,4 @@ def ago(date):
@register.filter(is_safe=True)
def amountformat(value, use_l10n=True):
return number_format(Decimal(value).quantize(Decimal('0.01')), force_grouping=True)
return number_format(Decimal(value).quantize(Decimal("0.01")), force_grouping=True)

View File

@ -1,4 +1,21 @@
import cStringIO as StringIO
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from __future__ import unicode_literals
import cgi
import json
import os.path
@ -6,14 +23,10 @@ import os.path
from django import http
from django.shortcuts import render
from django.template.loader import get_template
from django.template import RequestContext
from django.conf import settings
from django.contrib.staticfiles import finders
from weasyprint import HTML
import models
from weasyprint import default_url_fetcher, HTML
from . import models
def render_to_pdf(template_src, context_dict):
@ -22,12 +35,13 @@ def render_to_pdf(template_src, context_dict):
try:
pdf = html.write_pdf()
if hasattr(settings, 'FACTURE_DIR'):
facture = context['facture']
facture = context_dict['facture']
filename = os.path.join(
settings.FACTURE_DIR,
'%s-%s.pdf' % (facture.code(), facture.contrat.client.nom.encode('utf8')),
)
file(filename, 'w').write(pdf)
with open(filename, 'wb') as fd:
fd.write(pdf)
return http.HttpResponse(pdf, content_type='application/pdf')
except IOError:

View File

@ -4,21 +4,15 @@ import os.path
BASE_DIR = os.path.dirname(__file__)
DEBUG = False
ADMINS = (('Benjamin Dauvergne', 'bdauvergne@entrouvert.com'),)
MANAGERS = ADMINS
DEBUG = True
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['*']
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
'NAME': os.path.join(
os.path.dirname(__file__), 'facture.db'
), # Or path to database file if using sqlite3.
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'barbacompta',
'USER': '', # Not used with sqlite3.
'PASSWORD': '', # Not used with sqlite3.
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
@ -26,6 +20,25 @@ DATABASES = {
}
}
CACHES = {'default': {'BACKEND': 'django.core.cache.backends.db.DatabaseCache', 'LOCATION': 'cache',}}
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {'()': 'django.utils.log.RequireDebugFalse',},
'require_debug_true': {'()': 'django.utils.log.RequireDebugTrue',},
},
'formatters': {
'verbose': {
'format': '[%(asctime)s] %(ip)s %(user)s %(request_id)s %(levelname)s %(name)s.%(funcName)s: %(message)s',
'datefmt': '%Y-%m-%d %a %H:%M:%S',
},
},
'handlers': {'console': {'level': 'INFO', 'class': 'logging.StreamHandler', 'formatter': 'verbose',},},
'logger': {'django': {'handlers': ['console', 'mail_admins'], 'level': 'INFO',},},
}
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
@ -33,11 +46,11 @@ DATABASES = {
# timezone as the operating system.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'Europe/Paris'
TIME_ZONE = "Europe/Paris"
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'fr-fr'
LANGUAGE_CODE = "fr-fr"
SITE_ID = 1
@ -57,29 +70,26 @@ LOCALE_PATHS = (os.path.join(BASE_DIR, 'eo_gestion', 'locale'),)
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash if there is a path component (optional in other cases).
# Examples: "http://media.lawrence.com", "http://example.com/media/"
MEDIA_URL = '/media/'
MEDIA_URL = "/media/"
# URL prefix for static files.
# Example: "http://media.lawrence.com/static/"
STATIC_URL = '/static/'
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
# trailing slash.
# Examples: "http://foo.com/media/", "/media/".
ADMIN_MEDIA_PREFIX = STATIC_URL + 'admin/'
STATIC_URL = "/static/"
# Make this unique, and don't share it with anybody.
SECRET_KEY = '5w+ifr2ho!#x06q7dshr08wd#gt0wwp@wvbvw33kmtb+x$(9ts'
SECRET_KEY = "5w+ifr2ho!#x06q7dshr08wd#gt0wwp@wvbvw33kmtb+x$(9ts"
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
MIDDLEWARE = (
"django.middleware.common.CommonMiddleware",
"django.middleware.http.ConditionalGetMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
)
ROOT_URLCONF = 'eo_gestion.urls'
ROOT_URLCONF = "eo_gestion.urls"
# Templates
TEMPLATES = [
@ -103,40 +113,23 @@ TEMPLATES = [
]
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.sites",
"django.contrib.messages",
"django.contrib.staticfiles",
# Uncomment the next line to enable the admin:
'django.contrib.admin',
'eo_gestion.eo_facture',
'eo_gestion.eo_banque',
'mellon',
"django.contrib.admin",
"eo_gestion.eo_facture",
"eo_gestion.eo_banque",
)
AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend', 'mellon.backends.SAMLBackend')
AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
TEST_RUNNER = 'django.test.runner.DiscoverRunner'
LOGIN_REDIRECT_URL = "/"
LOGIN_REDIRECT_URL = '/'
MELLON_ATTRIBUTE_MAPPING = {
'username': '{attributes[username][0]}',
'email': '{attributes[email][0]}',
'first_name': '{attributes[first_name][0]}',
'last_name': '{attributes[last_name][0]}',
}
MELLON_SUPERUSER_MAPPING = {'is_superuser': (u'true',)}
MELLON_USERNAME_TEMPLATE = '{attributes[username][0]}'
MELLON_PUBLIC_KEYS = None
MELLON_PRIVATE_KEY = None
MELLON_IDENTITY_PROVIDERS = None
local_settings_file = os.environ.get('BARBACOMPTA_SETTINGS_FILE', 'local_settings.py')
local_settings_file = os.environ.get("BARBACOMPTA_SETTINGS_FILE", "local_settings.py")
if os.path.exists(local_settings_file):
execfile(local_settings_file)
with open(local_settings_file) as fd:
exec(fd.read())

View File

@ -1,25 +1,40 @@
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from __future__ import unicode_literals
from django.conf.urls import include, url
# Uncomment the next two lines to enable the admin:
from django.conf import settings
import django.contrib.admin
from django.views.generic.base import RedirectView
import admin
from . import admin
from eo_facture.views import api_references
from .eo_facture.views import api_references
django.contrib.admin.autodiscover()
urlpatterns = [
# Example:
# (r'^facturation/', include('facturation.foo.urls')),
# Uncomment the admin/doc line below and add 'django.contrib.admindocs'
# to INSTALLED_APPS to enable admin documentation:
# (r'^admin/doc/', include('django.contrib.admindocs.urls')),
# Uncomment the next line to enable the admin:
url(r'^favicon.ico', RedirectView.as_view(), {'url': '/static/img/favicon.ico'}),
url(r'^api/references/$', api_references),
url(r'^', include(admin.site.urls)),
url(r'^accounts/mellon/', include('mellon.urls')),
url(r'^', admin.site.urls),
]
if 'mellon' in settings.INSTALLED_APPS:
urlpatterns += [
url(r'^accounts/mellon/', include('mellon.urls')),
]

View File

@ -1,3 +1,21 @@
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from __future__ import unicode_literals
from decimal import Decimal

View File

@ -1,15 +1,14 @@
"""
'''
WSGI config for combo project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/
"""
'''
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "eo_gestion.settings")
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eo_gestion.settings')
from django.core.wsgi import get_wsgi_application

View File

@ -1,20 +0,0 @@
#!/bin/sh
# Get venv site-packages path
DSTDIR=`python -c 'from distutils.sysconfig import get_python_lib; print(get_python_lib())'`
# Get not venv site-packages path
# Remove first path (assuming that is the venv path)
NONPATH=`echo $PATH | sed 's/^[^:]*://'`
SRCDIR=`PATH=$NONPATH python -c 'from distutils.sysconfig import get_python_lib; print(get_python_lib())'`
# Clean up
rm -f $DSTDIR/lasso.*
rm -f $DSTDIR/_lasso.*
# Link
ln -sv $SRCDIR/lasso.py $DSTDIR
ln -sv $SRCDIR/_lasso.* $DSTDIR
exit 0

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
import os
import sys

85
pylint.django.rc Normal file
View File

@ -0,0 +1,85 @@
[MASTER]
profile=no
persistent=yes
ignore=migrations,south_migrations
cache-size=500
load-plugins=pylint_django
[MESSAGES CONTROL]
# C0111 Missing docstring
# I0011 Warning locally suppressed using disable-msg
# I0012 Warning locally suppressed using disable-msg
# W0704 Except doesn't do anything Used when an except clause does nothing but "pass" and there is no "else" clause
# W0142 Used * or * magic* Used when a function or method is called using *args or **kwargs to dispatch arguments.
# W0212 Access to a protected member %s of a client class
# W0232 Class has no __init__ method Used when a class has no __init__ method, neither its parent classes.
# W0613 Unused argument %r Used when a function or method argument is not used.
# W0702 No exception's type specified Used when an except clause doesn't specify exceptions type to catch.
# R0201 Method could be a function
disable=C0111,I0011,I0012,W0704,W0142,W0212,W0232,W0613,W0702,R0201,C0330
[REPORTS]
output-format=parseable
include-ids=yes
[BASIC]
no-docstring-rgx=__.*__|_.*
class-rgx=[A-Z_][a-zA-Z0-9_]+$
function-rgx=[a-zA_][a-zA-Z0-9_]{2,70}$
method-rgx=[a-z_][a-zA-Z0-9_]{2,70}$
const-rgx=(([A-Z_][A-Z0-9_]*)|([a-z_][a-z0-9_]*)|(__.*__)|register|urlpatterns)$
good-names=_,i,j,k,e,qs,pk,setUp,tearDown
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
ignored-classes=SQLObject,WSGIRequest
# When zope mode is activated, add a predefined set of Zope acquired attributes
# to generated-members.
zope=no
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed.
generated-members=objects,DoesNotExist,id,pk,_meta,base_fields,context
# List of method names used to declare (i.e. assign) instance attributes
defining-attr-methods=__init__,__new__,setUp
[VARIABLES]
init-import=no
dummy-variables-rgx=_|dummy
[SIMILARITIES]
min-similarity-lines=6
ignore-comments=yes
ignore-docstrings=yes
[MISCELLANEOUS]
notes=FIXME,XXX,TODO
[FORMAT]
max-line-length=160
max-module-lines=500
indent-string=' '
[DESIGN]
max-args=10
max-locals=15
max-returns=6
max-branchs=12
max-statements=50
max-parents=14
max-attributes=7
min-public-methods=0
max-public-methods=50

View File

@ -1,7 +1,7 @@
#!/bin/sh
set -e -x
env
set -e
if [ -f /var/lib/jenkins/pylint.django.rc ]; then
PYLINT_RC=/var/lib/jenkins/pylint.django.rc
elif [ -f pylint.django.rc ]; then
@ -10,4 +10,4 @@ else
echo No pylint RC found
exit 0
fi
pylint -f parseable --rcfile ${PYLINT_RC} "$@" | tee pylint.out || /bin/true
pylint -f parseable --rcfile ${PYLINT_RC} "$@" >pylint.out || /bin/true

View File

@ -1,10 +1,9 @@
#! /usr/bin/env python
from __future__ import print_function
from __future__ import print_function, unicode_literals
import os
import subprocess
import sys
from distutils.cmd import Command
from distutils.command.build import build as _build
@ -14,13 +13,8 @@ from setuptools.command.install_lib import install_lib as _install_lib
install_requires = [
'Django>=1.11,<1.12',
'reportlab<3',
'html5lib',
'weasyprint<0.43',
'django-mellon',
'django-model-utils<4',
'pyPdf',
'Pillow',
]
@ -53,7 +47,6 @@ def get_version():
class eo_sdist(sdist):
def run(self):
print("creating VERSION file")
if os.path.exists('VERSION'):
os.remove('VERSION')
version = get_version()
@ -61,7 +54,6 @@ class eo_sdist(sdist):
version_file.write(version)
version_file.close()
sdist.run(self)
print("removing VERSION file")
if os.path.exists('VERSION'):
os.remove('VERSION')
@ -77,18 +69,16 @@ class compile_translations(Command):
pass
def run(self):
try:
from django.core.management import call_command
from django.core.management import call_command
for path, dirs, files in os.walk('eo_gestion'):
if 'locale' not in dirs:
continue
curdir = os.getcwd()
os.chdir(os.path.realpath(path))
call_command('compilemessages')
os.chdir(curdir)
except ImportError:
sys.stderr.write('!!! Please install Django >= 1.4 to build translations\n')
os.environ.pop('DJANGO_SETTINGS_MODULE', None)
for path, dirs, files in os.walk('eo_gestion'):
if 'locale' not in dirs:
continue
curdir = os.getcwd()
os.chdir(os.path.realpath(path))
call_command('compilemessages')
os.chdir(curdir)
class build(_build):
@ -102,15 +92,15 @@ class install_lib(_install_lib):
setup(
name="barbacompta",
name='barbacompta',
version=get_version(),
license="AGPLv3 or later",
description="Logiciel de compta/facturation",
url="http://dev.entrouvert.org/projects/gestion",
author="Entr'ouvert",
author_email="info@entrouvert.org",
maintainer="Benjamin Dauvergne",
maintainer_email="bdauvergne@entrouvert.com",
license='AGPLv3 or later',
description='Logiciel de compta/facturation',
url='http://dev.entrouvert.org/projects/gestion',
author='Entr\'ouvert',
author_email='info@entrouvert.org',
maintainer='Benjamin Dauvergne',
maintainer_email='bdauvergne@entrouvert.com',
include_package_data=True,
scripts=['manage.py'],
packages=find_packages(),

61
tests/conftest.py Normal file
View File

@ -0,0 +1,61 @@
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
import pytest
import django_webtest
from django.core.management import call_command
from django.db import transaction
from django.contrib.auth.models import User
DATA = ["tests/fixture.json"]
@pytest.fixture(scope="session")
def base_db(django_db_setup, django_db_blocker):
with django_db_blocker.unblock():
with transaction.atomic():
try:
for data in DATA:
call_command("loaddata", data)
admin, created = User.objects.update_or_create(
username="admin",
defaults=dict(email="admin@example.com", is_superuser=True, is_staff=True),
)
admin.set_password("admin")
admin.save()
yield
finally:
transaction.set_rollback(True)
@pytest.fixture
def db(base_db):
yield
@pytest.fixture
def app(db, freezer):
freezer.move_to("2019-01-01")
wtm = django_webtest.WebTestMixin()
wtm._patch_settings()
try:
return django_webtest.DjangoTestApp(extra_environ={"HTTP_HOST": "localhost"})
finally:
wtm._unpatch_settings()

1
tests/fixture.json Normal file

File diff suppressed because one or more lines are too long

17
tests/settings.py Normal file
View File

@ -0,0 +1,17 @@
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
ALLOWED_HOSTS = ["localhost"]

43
tests/test_homepage.py Normal file
View File

@ -0,0 +1,43 @@
# barbacompta - accounting for dummies
# Copyright (C) 2010-2019 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from __future__ import unicode_literals
def test_homepage(app):
response = app.get("/").follow()
response.form.set("username", "admin")
response.form.set("password", "admin")
homepage = response.form.submit().follow()
clients = homepage.click("Clients")
str(clients)
ajouter_un_client = homepage.click("Ajouter un client")
str(ajouter_un_client)
contrats = homepage.click("Contrats")
contrat_2c210afd24c11596eeaf94bfb = contrats.click('2c210afd24c11596eeaf94bfb', href='change')
str(contrat_2c210afd24c11596eeaf94bfb)
ajouter_un_contrat = homepage.click("Ajouter un contrat")
str(ajouter_un_client)
compte_en_banque = homepage.click("Compte en banque")
str(compte_en_banque)
factures = homepage.click("Factures")
factures_00137 = factures.click('F2019.00137')
str(factures_00137)
rapid = factures.click('Rapid')
str(rapid)

View File

@ -1,7 +0,0 @@
import pytest
pytestmark = pytest.mark.django_db
def test_migrations(db):
pass

36
tox.ini
View File

@ -1,27 +1,37 @@
[tox]
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/barbacompta/{env:BRANCH_NAME:}
envlist = py2-django111-pylint
envlist = py2,py3
[testenv]
usedevelop = True
basepython = python2
setenv =
DJANGO_SETTINGS_MODULE=eo_gestion.settings
BARBACOMPTA_SETTINGS_FILE=tests/settings.py
deps =
django>=1.11,<1.12
psycopg2-binary
pytest
pytest-cov
pytest-django
pytest<4
WebTest
mock
httmock
pylint<1.8
pylint-django<0.8.1
django-webtest<1.9.3
pytest-freezegun
pylint
pylint-django
django-webtest
django-mellon
html5lib<1.0
pdbpp
commands =
./getlasso.sh
./pylint.sh eo_gestion/
py.test {posargs: --junitxml=test_{envname}_results.xml --cov-report xml --cov-report html --cov=eo_gestion/ tests/}
[testenv:pylint]
basepython = python3
deps=
django>=1.11,<1.12
pylint
pylint-django
uwsgidecorators
commands =
./pylint.sh eo_gestion/
[pytest]
filterwarnings=
ignore:Using or importing the ABCs from
junit_family=xunit2