Update billing system

This commit is contained in:
Gregory Trullemans 2024-06-16 11:07:59 +02:00
parent 6bd80f48e4
commit 30287e0ec0
37 changed files with 31011 additions and 313 deletions

View File

@ -1,5 +1,13 @@
# Comptabilité d'indépendant complémentaire
## Installation
Créez un environnement virtuel
`python3 -m venv /path/to/new/virtual/environment`
Installez les requirements
`pip install -r requirements/dev.txt`
## Application de comptabilité
Elle permet de gérer sa comptabilité (mouvement d'argent) d'indépendant complémentaire.

View File

@ -3,32 +3,40 @@ from django.contrib import admin
from .models import (
Client,
Contract,
DiscountContract,
Prestation,
PrestationType,
)
@admin.register(Client)
class ClientAdmin(admin.ModelAdmin):
model = Client
list_display = ('name', 'address', 'postal_code', 'city', 'contact')
search_fields = ('name', 'adress', 'city')
@admin.register(Contract)
class ContractAdmin(admin.ModelAdmin):
model = Contract
list_display = ('name', 'client', 'advance', 'reference', 'date')
search_fields = ('name', )
list_filter = ('is_finished', ) # 'date__year',
list_filter = ('is_finished', ) # 'date__year',
@admin.register(Prestation)
class PrestationAdmin(admin.ModelAdmin):
model = Prestation
list_display = ('date', 'label', 'total_amount', 'contract')
list_display = ('date', 'label', 'total_amount_htva', 'total_amount_tvac', 'contract')
search_fields = ('label', )
admin.site.register(Client, ClientAdmin)
admin.site.register(Contract, ContractAdmin)
admin.site.register(Prestation, PrestationAdmin)
@admin.register(PrestationType)
class PrestationTypeAdmin(admin.ModelAdmin):
model = PrestationType
list_display = ('label',)
search_fields = ('label',)
@admin.register(DiscountContract)
class DiscountContractAdmin(admin.ModelAdmin):
model = DiscountContract
list_display = ('contract', 'label', 'quantity', 'unit')

99
billing/forms.py Normal file
View File

@ -0,0 +1,99 @@
import csv
import math
import datetime
from django import forms
from datetime import date
from .models import Client, Contract, Prestation
# from django_select2.forms import Select2MultipleWidget, ModelSelect2Widget
class ClientForm(forms.ModelForm):
class Meta:
model = Client
fields = ("name", "address", "postal_code", "city", "contact", "company_number")
widgets = {
"name": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Nom du client"}
),
"address": forms.TextInput(
attrs={"class": "form-control col-sm-6 col-md-6 col-lg-6 col-xl-6", "placeholder": "Rue et numéro"}
),
"postal_code": forms.TextInput(
attrs={"class": "form-control col-md-2 col-lg-2 col-xl-2", "placeholder": "Code postal"}
),
"city": forms.TextInput(
attrs={"class": "form-control col-md-4 col-lg-4 col-xl-4", "placeholder": "Ville"}
),
"contact": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Nom du contact client"}
),
"is_company": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Routine's long name"}
),
"company_number": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Numéro BCE"}
)
}
class ContractForm(forms.ModelForm):
class Meta:
model = Contract
fields = ("title", "client", "advance", "reference", "is_finished", "is_paid", "description")
client = forms.ModelChoiceField(queryset=Contract.objects.all())
widgets = {
"title": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Titre du contrat"}
),
"client": forms.Select(
attrs={
"class": "form-control"
},
),
"advance": forms.TextInput(
attrs={"class": "form-control", }
),
"reference": forms.TextInput(
attrs={
"class": "form-control",
"placeholder": date.today().strftime("%Y-%m") + "-00x",
"value": date.today().strftime("%Y-%m") + "-00x",
}
),
"is_finished": forms.CheckboxInput(
attrs={"class": "form-control", }
),
"is_paid": forms.CheckboxInput(
attrs={"class": "form-control", }
),
"description": forms.Textarea(
attrs={"class": "form-control", "placeholder": "Description du contract."}
)
}
# class PrestationForm(forms.ModelForm):
# class Meta:
# model = Prestation
# fields = ("contract", "date", "label", "unit", "unit_price")
# widgets = {
# "contract": ModelSelect2Widget(
# search_fields=["contract__icontains",],
# max_results=10,
# attrs={"data-minimum-input-length": 0, "class": "form-control"},
# ),
# "date": forms.DateInput(
# attrs={
# "class": "form-control datepicker",
# "value": date.today().strftime("%Y-%m-%d"),
# }
# ),
# "label": forms.TextInput(
# attrs={"class": "form-control", }
# ),
# "unit": forms.TextInput(
# attrs={"class": "form-control", }
# ),
# "unit_price": forms.TextInput(
# attrs={"class": "form-control", }
# )
# }

View File

@ -0,0 +1,67 @@
# Generated by Django 4.0 on 2024-04-25 17:06
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Client',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Nom')),
('address', models.CharField(max_length=255, verbose_name='Adresse')),
('postal_code', models.IntegerField(verbose_name='Code postal')),
('city', models.CharField(max_length=255, verbose_name='Ville')),
('contact', models.CharField(max_length=255, verbose_name='Personne de contact')),
('email', models.EmailField(max_length=254)),
('phone_number', models.CharField(max_length=20, verbose_name='Téléphone')),
('company_number', models.CharField(blank=True, max_length=50, null=True, verbose_name="N° d'entreprise")),
],
options={
'verbose_name': 'Client',
'verbose_name_plural': 'Clients',
},
),
migrations.CreateModel(
name='Contract',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Nom')),
('advance', models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=6, verbose_name='Acompte')),
('reference', models.CharField(blank=True, max_length=255, null=True, verbose_name='Référence')),
('is_finished', models.BooleanField(blank=True, default=True)),
('date', models.DateField(auto_now_add=True)),
('invoiced_date', models.DateField(blank=True, default=None, null=True)),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='get_contract', to='billing.client')),
],
options={
'verbose_name': 'Contrat',
'verbose_name_plural': 'Contrats',
},
),
migrations.CreateModel(
name='Prestation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField()),
('label', models.CharField(max_length=255, verbose_name='Libellé')),
('unit', models.DecimalField(decimal_places=2, default=1, max_digits=5, verbose_name='Unité')),
('unit_price', models.DecimalField(decimal_places=2, default='12,5', max_digits=5, verbose_name='Prix unitaire')),
('total_amount', models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=6, verbose_name='Prix')),
('contract', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='get_prestation', to='billing.contract')),
],
options={
'verbose_name': 'Prestations',
'verbose_name_plural': 'Prestations',
'ordering': ['date'],
},
),
]

View File

@ -0,0 +1,55 @@
# Generated by Django 4.0 on 2024-04-26 13:28
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('billing', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='PrestationType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.CharField(max_length=255, verbose_name='Libellé')),
],
options={
'verbose_name': 'Type de prestations',
'verbose_name_plural': 'Types de prestations',
},
),
migrations.AddField(
model_name='prestation',
name='duration',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='prestation',
name='time_estimated',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='prestation',
name='time_tracked',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='prestation',
name='tva_value',
field=models.DecimalField(blank=True, decimal_places=2, default=21, max_digits=5, verbose_name='TVA'),
),
migrations.AlterField(
model_name='contract',
name='client',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contracts', to='billing.client'),
),
migrations.AddField(
model_name='prestation',
name='prestation_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='billing.prestationtype'),
),
]

View File

@ -0,0 +1,42 @@
# Generated by Django 4.0 on 2024-04-27 13:39
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('billing', '0002_prestationtype_prestation_duration_and_more'),
]
operations = [
migrations.CreateModel(
name='Estimate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Nom')),
('date', models.DateField(auto_now_add=True)),
('details', models.TextField()),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='estimates', to='billing.client')),
],
options={
'verbose_name': 'Devis',
'verbose_name_plural': 'Devis',
},
),
migrations.CreateModel(
name='Discount',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.CharField(max_length=255, verbose_name='Libellé')),
('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=5, verbose_name='Quantité')),
('unit', models.PositiveSmallIntegerField(choices=[(0, '%'), (1, '')], verbose_name='Unité')),
('contract', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='discounts', to='billing.contract')),
],
options={
'verbose_name': 'Réduction',
'verbose_name_plural': 'Réductions',
},
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 4.0 on 2024-04-27 14:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('billing', '0003_estimate_discount'),
]
operations = [
migrations.RemoveField(
model_name='prestation',
name='total_amount',
),
migrations.AddField(
model_name='prestation',
name='total_amount_htva',
field=models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=6, verbose_name='Prix (htva)'),
),
migrations.AddField(
model_name='prestation',
name='total_amount_tvac',
field=models.DecimalField(blank=True, decimal_places=2, default=0, max_digits=6, verbose_name='Prix (tvac)'),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 4.0 on 2024-06-16 05:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('billing', '0004_remove_prestation_total_amount_and_more'),
]
operations = [
migrations.RenameModel(
old_name='Discount',
new_name='DiscountContract',
),
migrations.AlterModelOptions(
name='discountcontract',
options={'verbose_name': 'Réduction contrat', 'verbose_name_plural': 'Réductions contrat'},
),
migrations.CreateModel(
name='DiscountClient',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.CharField(max_length=255, verbose_name='Libellé')),
('quantity', models.DecimalField(decimal_places=2, default=1, max_digits=5, verbose_name='Quantité')),
('unit', models.PositiveSmallIntegerField(choices=[(0, '%'), (1, '')], verbose_name='Unité')),
('client', models.ManyToManyField(to='billing.Client', verbose_name='discounts')),
],
options={
'verbose_name': 'Réduction client',
'verbose_name_plural': 'Réductions clients',
},
),
]

View File

View File

@ -1,6 +1,13 @@
from django.db import models
from django.db.models import Sum
from datetime import date
DISCOUNT_UNIT_CHOICE = [
(0, "%"),
(1, ""),
]
class Client(models.Model):
class Meta:
@ -12,8 +19,59 @@ class Client(models.Model):
postal_code = models.IntegerField(verbose_name="Code postal")
city = models.CharField(max_length=255, verbose_name="Ville")
contact = models.CharField(max_length=255, verbose_name="Personne de contact")
company_number = models.CharField(max_length=50, verbose_name="N° d'entreprise",
blank=True, null=True)
email = models.EmailField(max_length=254)
phone_number = models.CharField(max_length=20, verbose_name="Téléphone")
company_number = models.CharField(
max_length=50,
verbose_name="N° d'entreprise",
blank=True,
null=True
)
def __str__(self):
return "%s" % (self.name)
class DiscountClient(models.Model):
class Meta:
verbose_name = 'Réduction client'
verbose_name_plural = 'Réductions clients'
label = models.CharField(max_length=255, verbose_name='Libellé')
client = models.ManyToManyField(Client, verbose_name="discounts" )
quantity = models.DecimalField(
max_digits=5,
decimal_places=2,
verbose_name="Quantité",
default=1
)
unit = models.PositiveSmallIntegerField(
choices=DISCOUNT_UNIT_CHOICE, verbose_name="Unité"
)
def __str__(self):
return f"{self.label} - {self.quantity} {self.unit}"
# def amount_htva(self):
# if self.unit:
# return - self.quantity
# return (self.contract.get_contract_sum("total_amount_htva") * self.quantity / 100)
# def amount_tvac(self):
# if self.unit:
# return self.quantity * 0.21
# return (self.contract.get_contract_sum("total_amount_tvac") * self.quantity / 100)
class Estimate(models.Model):
class Meta:
verbose_name = 'Devis'
verbose_name_plural = 'Devis'
name = models.CharField(max_length=255, verbose_name="Nom")
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name="estimates")
date = models.DateField(auto_now_add=True)
details = models.TextField()
def __str__(self):
return "%s" % (self.name)
@ -25,15 +83,24 @@ class Contract(models.Model):
verbose_name_plural = 'Contrats'
name = models.CharField(max_length=255, verbose_name="Nom")
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name="get_contract")
advance = models.DecimalField(max_digits=6, decimal_places=2, blank=True,
verbose_name="Acompte", default=0)
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name="contracts")
advance = models.DecimalField(
max_digits=6,
decimal_places=2,
blank=True,
verbose_name="Acompte",
default=0
)
reference = models.CharField(max_length=255, verbose_name="Référence", blank=True, null=True)
is_finished = models.BooleanField(default=True, blank=True)
date = models.DateField(auto_now_add=True)
invoiced_date = models.DateField(default=None, blank=True, null=True)
def __str__(self):
return "%s" % (self.name)
return "%s" % (self.name)
def get_contract_sum(self, key):
return self.get_prestation.aggregate(Sum(key))[key + '__sum']
# def __generate_reference(self):
# """
@ -55,6 +122,15 @@ class Contract(models.Model):
# self.__generate_reference()
# super().save(*args, **kwargs)
class PrestationType(models.Model):
class Meta:
verbose_name = 'Type de prestations'
verbose_name_plural = 'Types de prestations'
label = models.CharField(max_length=255, verbose_name='Libellé')
def __str__(self):
return f"{self.label}"
class Prestation(models.Model):
class Meta:
@ -62,21 +138,59 @@ class Prestation(models.Model):
verbose_name_plural = 'Prestations'
ordering = ['date']
contract = models.ForeignKey(Contract, on_delete=models.CASCADE, related_name="get_prestation", blank=True, null=True)
contract = models.ForeignKey(
Contract,
on_delete=models.CASCADE,
related_name="get_prestation",
blank=True,
null=True
)
prestation_type = models.ForeignKey(PrestationType, blank=True, null=True, on_delete=models.SET_NULL)
date = models.DateField()
label = models.CharField(max_length=255, verbose_name='Libellé')
unit = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Unité",
default=1)
unit_price = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Prix unitaire",
default="12,5")
total_amount = models.DecimalField(max_digits=6, decimal_places=2, blank=True,
verbose_name="Prix", default=0)
unit = models.DecimalField(
max_digits=5,
decimal_places=2,
verbose_name="Unité",
default=1
)
unit_price = models.DecimalField(
max_digits=5,
decimal_places=2,
verbose_name="Prix unitaire",
default="12,5"
)
total_amount_htva = models.DecimalField(
max_digits=6,
decimal_places=2,
blank=True,
verbose_name="Prix (htva)",
default=0
)
tva_value = models.DecimalField(
max_digits=5,
decimal_places=2,
blank=True,
verbose_name="TVA",
default=21
)
total_amount_tvac = models.DecimalField(
max_digits=6,
decimal_places=2,
blank=True,
verbose_name="Prix (tvac)",
default=0
)
duration = models.PositiveIntegerField(null=True, blank=True)
time_estimated = models.PositiveIntegerField(null=True, blank=True)
time_tracked = models.PositiveIntegerField(null=True, blank=True)
def __compute_total_amount(self):
"""
Calcule le montant total de la prestation.
"""
self.total_amount = self.unit * self.unit_price
self.total_amount_htva = self.unit * self.unit_price
self.total_amount_tvac = self.unit * self.unit_price * self.tva_value
def save(self, *args, **kwargs):
"""
@ -84,3 +198,40 @@ class Prestation(models.Model):
"""
self.__compute_total_amount()
super().save(*args, **kwargs)
class DiscountContract(models.Model):
class Meta:
verbose_name = 'Réduction contrat'
verbose_name_plural = 'Réductions contrat'
contract = models.ForeignKey(
Contract,
on_delete=models.CASCADE,
related_name="discounts",
blank=True,
null=True
)
label = models.CharField(max_length=255, verbose_name='Libellé')
quantity = models.DecimalField(
max_digits=5,
decimal_places=2,
verbose_name="Quantité",
default=1
)
unit = models.PositiveSmallIntegerField(
choices=DISCOUNT_UNIT_CHOICE, verbose_name="Unité"
)
def __str__(self):
return f"{self.label} ({self.contract}) - {self.quantity} {self.unit}"
def amount_htva(self):
if self.unit:
return - self.quantity
return (self.contract.get_contract_sum("total_amount_htva") * self.quantity / 100)
def amount_tvac(self):
if self.unit:
return self.quantity * 0.21
return (self.contract.get_contract_sum("total_amount_tvac") * self.quantity / 100)

View File

@ -0,0 +1,59 @@
{% extends "base.html" %}
{% block content %}
<div id="page" class=" sidebar_right">
<div class="container">
<div id="frame2">
<div id="content">
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<h1>{% if client_id %}Modification{% else %}Ajout{% endif %} d'un client</h1>
</div>
</div>
<div class="row">
<div class="col-md-6 col-lg-6 col-xl-6 col-md-offset-3 col-lg-offset-3 col-xl-offset-3">
<div class="well well-small text-right">
<form action="{% if client_id %}{% url 'client_update' client_id %}{% else %}{% url 'client_create' %}{% endif %}" method="post" class="form-horizontal" id="formulaire" name="formulaire">
{% csrf_token %}
<div class="form-group row ">
<label for="id_date" class="col-4 col-sm-2 col-md-2 col-lg-2 col-xl-2 control-label">Nom<span class="text-danger">*</span></label>
<div class="col-12 col-sm-10 col-md-8 col-lg-8 col-xl-8 {% if form.name.errors %}has-danger{% endif %}">
{{ form.name }}
{% if form.name.errors %}<span class="btn btn-sm btn-danger-outline">{% for error in form.name.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group row ">
<label for="id_gymnast" class="col-4 col-sm-2 col-md-2 col-lg-2 col-xl-2 control-label">Adresse</label>
<div class="col-8 col-sm-8 col-md-6 col-lg-6 col-xl-6 {% if form.address.errors %}has-danger{% endif %}">
{{ form.address }}<br>
{{ form.postal_code }} {{ form.city }}
{% if form.address.errors %}&nbsp;<span class="btn btn-sm btn-danger-outline">{% for error in form.address.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group row ">
<label for="id_skill" class="col-4 col-sm-2 col-md-2 col-lg-2 col-xl-2 control-label">contact</label>
<div class="col-8 col-sm-8 col-md-6 col-lg-6 col-xl-6 {% if form.contact.errors %}has-danger{% endif %}">
{{ form.contact }}
{% if form.contact.errors %}&nbsp;<span class="btn btn-sm btn-danger-outline">{% for error in form.contact.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group row ">
<label for="id_information" class="col-4 col-sm-2 col-md-2 col-lg-2 col-xl-2 control-label">N° BCE</label>
<div class="col-8 col-sm-8 col-md-6 col-lg-6 col-xl-6 {% if form.company_number.errors %}has-danger{% endif %}">
{{ form.company_number }}
{% if form.company_number.errors %}&nbsp;<span class="btn btn-sm btn-danger-outline">{% for error in form.company_number.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group text-center">
<input type="submit" value="{% if client_id %}Sauvegarder{% else %}Ajouter{% endif %}" class="btn btn-primary" />
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block content %}
<div id="page" class=" sidebar_right">
<div class="container">
<div id="frame2">
<div id="content">
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<h1>Détails {{ client.name }}</h1>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<p><b>Nom Client : </b>{{ client.name }}</p>
<p><b>N° BCE : </b>{{ client.company_number }}</p>
<p><b>Nom de contact : </b>{{ client.contact }}</p>
<p><b>Adresse : </b>{{ client.address }} - {{ client.postal_code }} {{ client.city }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -15,6 +15,7 @@
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<table class="table table-striped table-bordered table-condensed">
<thead>
<th></th>
<th>Client</th>
<th>Contact</th>
<th>Adresse</th>
@ -23,7 +24,8 @@
</thead>
{% for client in client_list %}
<tr>
<td>{{ client.name }}</td>
<td></td>
<td><a href="{% url 'contract_listing_for_client' client.id %}">{{ client.name }}</a></td>
<td>{{ client.contact }}</td>
<td>{{ client.address }}</td>
<td>{{ client.postal_code }}</td>
@ -33,7 +35,7 @@
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,50 @@
{% extends "base.html" %}
{% block extra_head %}
{{ form.media.css }}
{% endblock %}
{% block content %}
<div id="page" class=" sidebar_right">
<div class="container">
<div id="frame2">
<div id="content">
<div class="row">
<div class="col-md-12 col-lg-12">
<h1>{% if contract_id %}Modification{% else %}Nouveau{% endif %} contract</h1>
</div>
</div>
<div class="row">
<div class="col-md-6 col-l-6 col-xl-6 col-md-offset-3 col-l-offset-3 col-xl-offset-3">
<div class="well well-small text-right">
<form action="{% if contract_id %}{% url 'contract_update' contract_id %}{% else %}{% url 'contract_create' %}{% endif %}" method="post" class="form-horizontal" id="formulaire" name="formulaire">
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id }}" class="col-lg-2 control-label">{{ field.label_tag }}</label>
<div class="col-lg-8">
{{ field }}
{% if field.errors %}<span class="btn btn-sm btn-danger-outline">{% for error in field.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
{% endfor %}
<div class="form-group text-center">
<input type="submit" value="{% if client_id %}Sauvegarder{% else %}Modifier{% endif %}" class="btn btn-warning" />
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_script %}
{{ form.media.js }}
{% endblock %}

View File

@ -0,0 +1,67 @@
{% extends "base.html" %}
{% block content %}
<div id="page" class=" sidebar_right">
<div class="container">
<div id="frame2">
<div id="content">
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<h1>{{ contract.name }}</h1>
<h4 class="text-muted">{{ contract.client }}</h4>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-10 col-md-offset-1">
{% if prestations_list %}
<table class="table table-striped table-bordered table-condensed">
<thead>
<th>Date</th>
<th>Prestation</th>
<th class="centered">Quantité</th>
<th class="centered">Prix u.</th>
<th class="centered">Total (htva)</th>
<th class="centered">Total (tvac)</th>
</thead>
{% for prestation in prestations_list %}
<tr>
<td>{{ prestation.date|date:"d-m-Y" }}</td>
<td>{{ prestation.label }}</td>
<td class="push-right">{{ prestation.unit }}</td>
<td class="push-right">{{ prestation.unit_price }}</td>
<td class="push-right">{{ prestation.total_amount_htva }} €</td>
<td class="push-right">{{ prestation.total_amount_tvac }} €</td>
</tr>
{% endfor %}
<tr>
<td colspan="4" class="push-right">Total (sans réduction) :</td>
<td class="push-right"><b>{{ total_without_discount_htva|floatformat:2 }} €</b></td>
<td class="push-right"><b>{{ total_without_discount_tvac|floatformat:2 }} €</b></td>
</tr>
{% for discount in contract.discounts.all %}
<tr>
<td colspan="4">{{ discount.label }} (-{{ discount.quantity }}{{ discount.get_unit_display }})</td>
<td class="push-right">-{{ discount.amount_htva|floatformat:2 }} €</td>
<td class="push-right">-{{ discount.amount_tvac|floatformat:2 }} €</td>
</tr>
{% endfor %}
<tr>
<td colspan="4" class="push-right"><b>Total :</b></td>
<td class="push-right"><b>{{ total_with_discount_htva|floatformat:2 }} €</b></td>
<td class="push-right"><b>{{ total_with_discount_tvac|floatformat:2 }} €</b></td>
</tr>
</table>
<p class="right">
<a href="{% url 'contract_export' contract.id %}" class="btn btn-default btn-primary" role="button">Facture PDF</a>
</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -8,16 +8,19 @@
<div id="content">
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<h1>Liste des contrats</h1>
<h1>Liste des contrats {% if client %}pour <i>{{ client }}</i>{% endif %}</h1>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
{% if contract_list %}
<table class="table table-striped table-bordered table-condensed">
<thead>
<th class="centered">Année</th>
<th>Nom</th>
{% if not client %}
<th class="centered">Client</th>
{% endif %}
<th class="centered">Acompte</th>
<th class="centered"># Prest.</th>
</thead>
@ -25,15 +28,18 @@
<tr>
<td class="centered">{{ contract.date.year }}</td>
<td><a href="{% url 'contract_detail' contract.id %}">{{ contract.name }}</a></td>
{% if not client %}
<td>{{ contract.client }}</td>
{% endif %}
<td class="push-right">{{ contract.advance }}</td>
<td class="push-right">{{ contract.get_prestation.count }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,99 @@
{% extends "base.html" %}
{% block content %}
<div id="page" class=" sidebar_right">
<div class="container">
<div id="frame2">
<div id="content">
<div class="row">
<div class="col-md-12 col-lg-12">
<h1>{% if prestation_id %}Modification{% else %}Nouvelle{% endif %} prestation</h1>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-10 col-lg-8 col-md-offset-1 col-lg-offset-2">
<div class="well well-small text-right">
<form action="{% if prestation_id %}{% url 'prestation_update' prestation_id %}{% else %}{% url 'prestation_create' %}{% endif %}" method="post" class="form-horizontal" id="formulaire" name="formulaire">
{% csrf_token %}
<div class="form-group row ">
<label for="id_contract" class="col-sm-4 col-md-4 col-lg-3 col-xl-2 control-label">{{ form.contract.label }}</label>
<div class="col-sm-6 col-md-6 col-lg-6 col-xl-6 {% if form.contract.errors %}has-danger{% endif %}">
{{ form.contract }}
{% if form.contract.errors %}&nbsp;<span class="btn btn-sm btn-danger-outline">{% for error in form.contract.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group row ">
<label for="id_date" class="col-sm-4 col-md-4 col-lg-3 col-xl-2 control-label">{{ form.date.label }}</label>
<div class="col-sm-3 col-md-3 col-lg-3 col-xl-3 {% if form.date.errors %}has-danger{% endif %}">
{{ form.date }}
{% if form.date.errors %}&nbsp;<span class="btn btn-sm btn-danger-outline">{% for error in form.date.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group row ">
<label for="id_label" class="col-sm-4 col-md-4 col-lg-3 col-xl-2 control-label">{{ form.label.label }}</label>
<div class="col-sm-6 col-md-6 col-lg-8 col-xl-8 {% if form.label.errors %}has-danger{% endif %}">
{{ form.label }}
{% if form.label.errors %}&nbsp;<span class="btn btn-sm btn-danger-outline">{% for error in form.label.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group row ">
<label for="id_unit" class="col-sm-4 col-md-4 col-lg-3 col-xl-2 control-label">{{ form.unit.label }}</label>
<div class="col-sm-3 col-md-3 col-lg-2 col-xl-2 {% if form.unit.errors %}has-danger{% endif %}">
{{ form.unit }}
{% if form.unit.errors %}&nbsp;<span class="btn btn-sm btn-danger-outline">{% for error in form.unit.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group row ">
<label for="id_unit_price" class="col-sm-4 col-md-4 col-lg-3 col-xl-2 control-label">{{ form.unit_price.label }}</label>
<div class="col-sm-3 col-md-3 col-lg-2 col-xl-2 {% if form.unit_price.errors %}has-danger{% endif %}">
{{ form.unit_price }}
{% if form.unit_price.errors %}&nbsp;<span class="btn btn-sm btn-danger-outline">{% for error in form.unit_price.errors %}{{error}}{% endfor %}</span>{% endif %}
</div>
</div>
<div class="form-group text-center">
<input type="submit" value="{% if prestation_id %}Modifier{% else %}Sauvegarder{% endif %}" class="btn btn-primary" />
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript" >
$(function(){
$('#id_contract_related').autocomplete({
source: function(request, response){
$.ajax({
url: '/billing/contract/lookup/?pattern=' + $('#id_contract_related').val(),
dataType: "json",
success: function(data){
if(data.length != 0){
response($.map(data, function(item){
return {
label: item.Name,
value: item.Name,
contractid: item.ID
}
}))
} else {
response([{ label: 'No result found.', value: '' }]);
};
},
error: function(exception){
console.log(exception);
}
});
},
minLength: 3,
select: function(event, ui){
$($(this).data('ref')).val(ui.item.contractid);
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,137 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="keywords" content="">
<meta name="description" content="">
<meta name="author" content="Gregory Trullemans">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="apple-touch-icon" sizes="76x76" href="{% static "img/apple-icon.png" %}">
<link rel="icon" type="image/png" href="{% static "img/favicon.png" %}">
<title>Facture</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@200..900&display=swap" rel="stylesheet" />
<link href="{% static "css/a4_paper.css" %}" rel="stylesheet" />
<link href="{% static "css/black-dashboard_report.css" %}" rel="stylesheet" />
<link href="{% static "css/to_pdf.css" %}" rel="stylesheet" />
</head>
<body class="white-content">
<div class="row no-gutters">
<div id="header-left" class="col-6">
<img src="{% static 'img/visite2.png' %}" style="width:95%" />
</div>
<div id="header-right" class="col-6 text-right pr-0">
<h1 class="main_title">Facture</h1>
</div>
</div>
<br />
<div class="row no-gutters bordered_top bordered_bottom">
<div class="col-6" id="contract_reference">
<p><b>Date d'émission</b> : {{ date|date:"j F Y" }}</p>
</div>
<div class="col-6 text-right">
<p><b>Référence</b> : {{ contract.reference }}</p>
<br />
</div>
</div>
<br />
<div class="row no-gutters" id="client_references">
<div id="header-left" class="col-6 text-left bordered_bottom">
<p><b>Arnaud Croonen</b></p>
<p>Rue landuyt 26, boite 0201</p>
<p>1440 Braine-le-château</p>
<p><b>Mail</b> : info@arnaudcroonen.be</p>
<p><b>Tel</b> : 0472 42 55 56</p>
<p><b>IBAN</b> : BE30 0689 5201 4611</p>
<p><b>TVA</b> : BE 1008.358.946</p>
<br />
</div>
<div id="header-right" class="col-6 text-right bordered_bottom">
<p>à l'attention de <em>{{ client.contact }}</em></p>
<p><b>{{ client.name }}</b></p>
<p>{{ client.address }}</p>
<p>{{ client.postal_code }} {{ client.city }}</p>
<p><b>Mail</b> : wimauto@skynet.be</p>
<p><b>Tel</b> : 0472 42 55 56</p>
<p><b>TVA</b> : {{ client.company_number }}</p>
<br />
</div>
</div>
<br />
<div class="row no-gutters">
<div class="col-12">
<table class="table">
<thead>
<tr class="bordered-row bordered_bottom">
<th class="header text-left pl-0 bordered_bottom" style="width: 35%">Description</th>
<th class="header text-right bordered_bottom" style="width: 15%">Durée</th>
<th class="header text-right bordered_bottom" style="width: 10%">Quantité</th>
<th class="header text-right bordered_bottom" style="width: 12%">Prix u.</th>
<th class="header text-right bordered_bottom" style="width: 11%">Prix (htva)</th>
<th class="header text-right bordered_bottom" style="width: 11%">Prix (tvac)</th>
</tr>
</thead>
<tbody>
{% for prestation in prestations_list %}
<tr>
<td class="pt-2 pb-2 pl-3">{{ prestation.label }}</td>
<td class="pt-2 pb-2 pl-3 text-right">{{ prestation.date|date:"j N Y" }}</td>
<td class="pt-2 pb-2 text-right">{{ prestation.unit }}</td>
<td class="pt-2 pb-2 text-right">{{ prestation.unit_price }}€</td>
<td class="pt-2 pb-2 text-right">{{ prestation.total_amount_htva }} €</td>
<td class="pt-2 pb-2 text-right">{{ prestation.total_amount_tvac }} €</td>
</tr>
{% endfor %}
<tr class="bordered-row">
<td colspan="4" class="push-right">Sous-Total (sans réduction) :</td>
<td class="text-right"><b>{{ total_without_discount_htva|floatformat:2 }} €</b></td>
<td class="text-right"><b>{{ total_without_discount_tvac|floatformat:2 }} €</b></td>
</tr>
{% for discount in contract.discounts.all %}
<tr>
<td colspan="4">{{ discount.label }} (-{{ discount.quantity }}{{ discount.get_unit_display }})</td>
<td class="text-right">-{{ discount.amount_htva|floatformat:2 }} €</td>
<td class="text-right">-{{ discount.amount_tvac|floatformat:2 }} €</td>
</tr>
{% endfor %}
<tfoot>
<tr class="bordered-row">
<td colspan="4" class="pl-0"><strong>Total</strong></td>
<td class="text-right"><b>{{ total_with_discount_htva|floatformat:2 }} €</b></td>
<td class="text-right"><b>{{ total_with_discount_tvac|floatformat:2 }} €</b></td>
</tr>
</tfoot>
</table>
</div>
</div>
<br />
<div class="row no-gutters pdf_footer">
<div class="col-12 bordered_top bordered_bottom bordered_right bordered-left text-center">
<p>Merci de verser l'intégralité de ce montant sur le compte IBAN - <b>BE30 0689 5201 4611</b> dans les 15 jours avec la référence <b>{{ contract.reference }}</b>.</p>
<p>Les produits audiovisuels finaux seront délivrés dès réception du paiement.</p>
<p><em>Merci pour votre confiance.</em></p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,129 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="keywords" content="">
<meta name="description" content="">
<meta name="author" content="Gregory Trullemans">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Facture</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@200..900&display=swap" rel="stylesheet" />
<link href="{% static "css/a4_paper.css" %}" rel="stylesheet" />
<link href="{% static "css/black-dashboard_report.css" %}" rel="stylesheet" />
<link href="{% static "css/to_pdf.css" %}" rel="stylesheet" />
</head>
<header class="white-content">
<div class="row ml-0 mr-0">
<div id="header-left" class="col-6">
<img src="{% static 'img/visite2.png' %}" />
</div>
<div id="header-right" class="col-6 text-right">
<h1 class="main_title">Facture</h1>
</div>
</div>
</header>
<br />
<body class="white-content">
<div class="row ml-0 mr-0">
<div class="col-6 bordered_top" id="contract_reference">
<p><b>Date d'émission</b> : 24/04/2024</p>
</div>
<div class="col-6 text-right bordered_top">
<p><b>Référence</b> : {{ contract.reference }}</p>
</div>
<br />
<br />
</div>
<div class="row ml-0 mr-0" id="client_references">
<div id="header-left" class="col-6 text-left bordered_top">
<p><b>Arnaud Croonen</b></p>
<p>Rue landuyt 26, boite 0201</p>
<p>1440 Braine-le-château</p>
<p><b>Mail</b> : info@arnaudcroonen.be</p>
<p><b>Tel</b> : 0472 42 55 56</p>
<p><b>IBAN</b> : BE30 0689 5201 4611</p>
<p><b>TVA</b> : BE 1008.358.946</p>
</div>
<div id="header-right" class="col-6 text-right bordered_top">
<p>à l'attention de <em>Wim Vanderheyden</em></p>
<p><b>Concept Cycles</b></p>
<p>Route Provinciale, 96</p>
<p>1480 Clabecq</p>
<p><b>Mail</b> : wimauto@skynet.be</p>
<p><b>TVA</b> : 0611.842.841</p>
</div>
</div>
<div class="row ml-0 mr-0">
<div class="col-12">
<table class="table">
<thead>
<tr>
<th>Description</th>
<th>Durée</th>
<th class="text-center">Quantité</th>
<th class="text-center">Prix unitaire</th>
<th class="text-center">Prix</th>
</tr>
</thead>
<tbody>
{% for prestation in prestations_list %}
<tr>
<td class="pt-1 pb-1">{{ prestation.date|date:"d-m-Y" }}</td>
<td class="pt-1 pb-1">{{ prestation.label }}</td>
<td class="pt-1 pb-1 text-right">{{ prestation.unit }}</td>
<td class="pt-1 pb-1 text-right">{{ prestation.unit_price }}€</td>
<td class="pt-1 pb-1 text-right">{{ prestation.total_amount }}€</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="4"><strong>Sous-total (HTVA)</strong></td>
<td class="text-right">{{ total }}€</td>
</tr>
<tr>
<td colspan="4">Réduction "premiers clients" - 10%</td>
<td class="text-right">-50€</td>
</tr>
<tr>
<td colspan="4"><strong>Total HTVA</strong></td>
<td class="text-right">450€</td>
</tr>
<tr>
<td colspan="4">TVA - 21%</td>
<td class="text-right">94.50€</td>
</tr>
<tr>
<td colspan="4"><strong>Total TVAC</strong></td>
<td class="text-right">544.50€</td>
</tr>
</tfoot>
</table>
</div>
</div>
<br />
<div class="row ml-0 mr-0">
<div class="col-12 bordered_top bordered_bottom bordered_right bordered-left text-center">
<p>Merci de verser l'intégralité de ce montant sur le compte IBAN - <b>BE30 0689 5201 4611</b> dans les 15 jours avec la référence <b>202404-001</b>.</p>
<p>Les produits audiovisuels finaux seront délivrés dès réception du paiement.</p>
<p><em>Merci pour votre confiance.</em></p>
</div>
</div>
</body>
</html>

View File

@ -3,8 +3,9 @@ from django.urls import path, re_path
from . import views
billing_urlpatterns = [
path(r"contract/pdf/<int:contractid>/", views.contract_export, name="contract_export"),
path(r"contract/<int:contractid>/", views.contract_detail, name="contract_detail"),
path(r"contract/pdf/<int:contract_id>/", views.contract_export, name="contract_export"),
path(r"contract/<int:contract_id>/", views.contract_detail, name="contract_detail"),
path(r"contract/client/<int:client_id>/", views.contract_listing, name="contract_listing_for_client"),
path(r"contract", views.contract_listing, name="contract_listing"),
path(r"prestation", views.prestation_listing, name="prestation_listing"),
path(r"client", views.client_listing, name="client_listing"),

View File

@ -1,14 +1,15 @@
from django.shortcuts import render
from django.shortcuts import render, get_object_or_404
from django.template import RequestContext
from django.http import HttpResponse
from django.db.models import Sum
from reportlab.pdfgen import canvas
from reportlab.lib.colors import Color, black, blue, red
from reportlab.lib.pagesizes import A4
from django.template.loader import render_to_string
from weasyprint import HTML, CSS
# from weasyprint.fonts import FontConfiguration
# from PIL import Image
import os
import pendulum
from django.conf import settings
from datetime import date
@ -22,31 +23,47 @@ from .models import (
)
def contract_listing(request):
def contract_listing(request, client_id=None):
"""
Renvoie la liste de tous les contrats.
"""
contract_list = Contract.objects.all()
context = {'contract_list': contract_list}
return render(request, 'billing/contract/listing.html', context)
if client_id:
client = get_object_or_404(Client, pk=client_id)
contract_list = client.contracts.all()
else:
client = None
contract_list = Contract.objects.all()
context = {"contract_list": contract_list, "client": client}
return render(request, "contract/listing.html", context)
def contract_detail(request, contractid):
def contract_detail(request, contract_id):
"""
Renvoie toutes les informations relatives à un contrat, en ce y compris les prestations
relatives à celui-ci.
"""
contract = Contract.objects.get(pk=contractid)
prestation_list = contract.get_prestation.all()
prestation_count = prestation_list.count()
total = list(contract.get_prestation.all().aggregate(Sum('total_amount')).values())[0]
context = {'contract': contract,
'prestation_list': prestation_list,
'prestation_count': prestation_count,
'total': total}
return render(request, 'billing/contract/detail.html', context)
contract = Contract.objects.get(pk=contract_id)
total_without_discount_htva = contract.get_contract_sum("total_amount_htva")
total_without_discount_tvac = contract.get_contract_sum("total_amount_tvac")
total_with_discount_htva = total_without_discount_htva
total_with_discount_tvac = total_without_discount_tvac
for discount in contract.discounts.all():
total_with_discount_htva -= discount.amount_htva()
total_with_discount_tvac -= discount.amount_tvac()
context = {
"contract": contract,
"prestations_list": contract.get_prestation.all(),
"total_without_discount_htva": total_without_discount_htva,
"total_without_discount_tvac": total_without_discount_tvac,
"total_with_discount_htva": total_with_discount_htva,
"total_with_discount_tvac": total_with_discount_tvac,
}
return render(request, "contract/detail.html", context)
def client_listing(request):
@ -54,8 +71,8 @@ def client_listing(request):
Renvoie la liste de tous les clients.
"""
client_list = Client.objects.all()
context = {'client_list': client_list}
return render(request, 'billing/client/listing.html', context)
context = {"client_list": client_list}
return render(request, "client/listing.html", context)
def prestation_listing(request):
@ -63,229 +80,59 @@ def prestation_listing(request):
Renvoie la liste de toutes les prestations.
"""
prestation_list = Prestation.objects.all()
context = {'prestation_list': prestation_list}
return render(request, 'billing/prestation/listing.html', context)
context = {"prestation_list": prestation_list}
return render(request, "prestation/listing.html", context)
Y = 841.89
X = 35
RIGHT_X = 595.27 - X
TITLED_X = 125
INDENTED_X = X + 5
INDENTED_RIGHT_X = RIGHT_X - 5
def contract_export(request, contract_id):
"""Génere un fichier PDF à fournir au client."""
contract = get_object_or_404(Contract, pk=contract_id)
PRESTATION_COLUMN_2 = INDENTED_X + 65
PRESTATION_COLUMN_3 = INDENTED_X + 400
PRESTATION_COLUMN_4 = INDENTED_X + 455
if not contract.invoiced_date:
today = pendulum.now().date()
contract.invoiced_date = today
contract.save()
COMMON_LINE_HEIGHT = -15
DOUBLE_LINE_HEIGHT = COMMON_LINE_HEIGHT * 2
BIG_LINE_HEIGHT = COMMON_LINE_HEIGHT * 3
HUGE_LINE_HEIGHT = COMMON_LINE_HEIGHT * 4
contract = Contract.objects.get(pk=contract_id)
total_without_discount_htva = contract.get_contract_sum("total_amount_htva")
total_without_discount_tvac = contract.get_contract_sum("total_amount_tvac")
class MyDocument(object):
# Create the PDF object, using the response object as its "file."
# http://www.reportlab.com/docs/reportlab-userguide.pdf
# canvas.rect(x, y, width, height, stroke=1, fill=0)
# localhost:8000/billing/contract/pdf/2
total_with_discount_htva = total_without_discount_htva
total_with_discount_tvac = total_without_discount_tvac
for discount in contract.discounts.all():
total_with_discount_htva -= discount.amount_htva()
total_with_discount_tvac -= discount.amount_tvac()
def __init__(self, response):
# Create the PDF object, using the response object as its "file."
self.document = canvas.Canvas(response, pagesize=A4)
self.y = Y - X
context = {
"contract": contract,
"client": contract.client,
"date": contract.invoiced_date,
"prestations_list": contract.get_prestation.all(),
# "discounts_list": discounts_list,
# "discount_result": discount_result,
# "prestations_count": prestations_count,
"total_without_discount_htva": total_without_discount_htva,
"total_without_discount_tvac": total_without_discount_tvac,
"total_with_discount_htva": total_with_discount_htva,
"total_with_discount_tvac": total_with_discount_tvac,
}
def newline(self, height=None):
"""
Passe à la ligne, la hauteur de la ligne étant passée en paramètre.
"""
if height == DOUBLE_LINE_HEIGHT:
self.y += DOUBLE_LINE_HEIGHT
elif height == BIG_LINE_HEIGHT:
self.y += BIG_LINE_HEIGHT
elif height == HUGE_LINE_HEIGHT:
self.y += HUGE_LINE_HEIGHT
else:
self.y += COMMON_LINE_HEIGHT
# return render(request, "to_pdf/facture.html", context)
# if y < 120;
# document.PageBreak()
# y = 790
html = render_to_string("to_pdf/facture.html", context)
response = HttpResponse(content_type="application/pdf")
response[
"Content-Disposition"
] = f"attachment; filename=facture.pdf" # pylint: disable=line-too-long
def drawString(self, x, string, font_family="Helvetica", font_decoration=None, font_size=10):
font = font_family
if font_decoration is not None:
font += "-" + font_decoration
self.document.setFont(font, font_size)
self.document.drawString(x, self.y, string)
# font_config = FontConfiguration()
HTML(string=html, base_url=request.build_absolute_uri()).write_pdf(
response,
stylesheets=[
CSS(settings.STATICFILES_DIRS[0] + "/css/a4_paper.css"),
CSS(settings.STATICFILES_DIRS[0] + "/css/black-dashboard_report.css"),
CSS(settings.STATICFILES_DIRS[0] + "/css/to_pdf.css"),
],
) # , font_config=font_config)
def drawNewLine(self, x, string, height=None, font_family="Helvetica", font_decoration=None, font_size=10):
self.newline(height)
self.drawString(x, string, font_family, font_decoration, font_size)
def header(self, info, contract):
"""
Génère le header de la facture.
"""
# self.document.rect(X, 735, 525, 70, fill=0)
self.drawNewLine(INDENTED_X, info['NAME'])
self.document.drawRightString(RIGHT_X, self.y, "N° de Référence : " + str(contract.reference))
self.drawNewLine(INDENTED_X, info['ADDRESS'])
self.drawNewLine(INDENTED_X, info['CITY'])
self.drawNewLine(INDENTED_X, info['GSM'])
self.newline(DOUBLE_LINE_HEIGHT)
def title(self, contract):
"""
Génère le "titre" de la facture.
"""
theString = "A l'attention de"
self.drawString(TITLED_X, "A l'attention de")
self.drawString(194, contract.client.contact, font_decoration="Bold")
self.drawString(194 + (len(contract.client.contact) * 5.2), " pour la")
self.drawNewLine(TITLED_X, contract.client.name, font_decoration="Bold")
self.drawNewLine(TITLED_X, contract.client.address)
self.drawNewLine(TITLED_X, str(contract.client.postal_code) + " " + contract.client.city)
self.newline()
self.drawNewLine(TITLED_X, "Concernant la/le")
self.drawString(200, contract.name, font_decoration="Bold")
self.newline()
def payementInformation(self, info, contract):
"""
Génère les informations de payement.
"""
self.newline()
height = 40
self.document.rect(X, self.y - height, RIGHT_X - X, height, fill=0)
self.drawNewLine(INDENTED_X, "N° Entreprise : " + info['COMPANY_NUMBER'])
self.document.drawRightString(INDENTED_RIGHT_X, self.y, "Votre N° Entreprise : " + contract.client.company_number)
self.drawNewLine(INDENTED_X, "N° de compte : " + info['BANK'] + " - " + info['ACCOUNT'])
self.newline(DOUBLE_LINE_HEIGHT)
def prestations(self, contract):
"""
Génère l'affichage des prestations : tableau, liste des prestations, …
"""
self.drawNewLine(X, "Prestations", font_decoration="Bold")
total = self.drawPrestationsTable(contract.get_prestation.all())
self.newline(DOUBLE_LINE_HEIGHT)
self.document.setFont("Helvetica", 10)
self.document.drawRightString(INDENTED_X + 445, self.y, "Acompte")
self.document.drawRightString(INDENTED_RIGHT_X, self.y, str(contract.advance))
self.newline()
self.document.setFont("Helvetica-Bold", 10)
self.document.drawRightString(INDENTED_X + 445, self.y, "Solde à payer")
self.document.drawRightString(INDENTED_RIGHT_X, self.y, str(total - contract.advance))
def drawColoredRow(self):
"""
Génère une ligne colorée.
"""
self.document.setFillColorCMYK(0.43,0.2,0,0)
self.document.rect(X, self.y - 4, RIGHT_X - X, COMMON_LINE_HEIGHT, fill=True, stroke=False)
self.document.setFillColorCMYK(0,0,0,1)
def drawHeaderPrestationsTable(self):
"""
Génère le header de la table des prestations.
"""
self.drawColoredRow()
self.newline()
self.document.setFillColorCMYK(0,0,0,1)
self.drawString(INDENTED_X, "Date")
self.drawString(PRESTATION_COLUMN_2, "Libellé")
self.drawString(INDENTED_X + 365, "Nbre")
self.drawString(INDENTED_X + 420, "Prix Unit.")
self.document.setFont("Helvetica-Bold", 10)
self.document.drawRightString(INDENTED_X + 510, self.y, "Total")
def drawFooterPrestationTable(self, total):
"""
Génère le footer de la table des prestations.
"""
self.drawColoredRow()
self.newline()
self.document.setFont("Helvetica-Bold", 10)
self.document.drawRightString(INDENTED_X + 445, self.y, "Total")
self.document.drawRightString(INDENTED_RIGHT_X, self.y, str(total))
def displayPrestation(self, prestation, total):
"""
Affiche une ligne de prestations dans le tableau.
"""
# self.newline()
total += prestation.total_amount
self.drawNewLine(INDENTED_X, str(prestation.date))
self.drawString(PRESTATION_COLUMN_2, prestation.label)
self.document.drawRightString(PRESTATION_COLUMN_3, self.y, str(prestation.unit))
# self.document.drawRightString(INDENTED_X + 400, self.y, str(prestation.unit))
self.document.drawRightString(PRESTATION_COLUMN_4, self.y, str(prestation.unit_price))
# self.document.drawRightString(INDENTED_X + 455, self.y, str(prestation.unit_price))
self.document.drawRightString(INDENTED_RIGHT_X, self.y, str(prestation.total_amount))
return total
def drawPrestationsTable(self, prestation_list):
"""
Génère le tableau des prestations.
"""
self.drawHeaderPrestationsTable()
total = 0
for prestation in prestation_list:
total = self.displayPrestation(prestation, total)
self.drawFooterPrestationTable(total)
return total
def conclusion(self, info, contract):
"""
Affiche la conclusion de la facture.
"""
self.newline(DOUBLE_LINE_HEIGHT)
self.document.rect(X, self.y, RIGHT_X - X, BIG_LINE_HEIGHT, fill=0)
self.drawNewLine(INDENTED_X, "Merci de bien vouloir payer sur le compte ")
self.drawString(INDENTED_X + 185, info['BANK'] + " : " + info['ACCOUNT'], font_decoration="Bold")
self.newline(COMMON_LINE_HEIGHT)
self.drawString(INDENTED_X, "Avec la référence :")
self.drawString(INDENTED_X + 85, "\"" + str(contract.reference) + "\"", font_decoration="Bold")
def addSignature(self):
"""
Génère la signature.
"""
self.newline(BIG_LINE_HEIGHT)
self.document.drawRightString(RIGHT_X, self.y, "Rebecq, le " + date.today().strftime('%d-%m-%Y'))
self.newline(COMMON_LINE_HEIGHT)
url = os.path.join(settings.STATICFILES_DIRS[0], 'img/signature.png')
self.newline(DOUBLE_LINE_HEIGHT)
im = self.document.drawImage(url, INDENTED_X + 340, self.y, width=180, height=39)
# im.hAlign = 'CENTER'
def download(self):
# Close the PDF object cleanly, and we're done.
self.document.showPage()
self.document.save()
def contract_export(request, contractid):
"""
Génere une fichier PDF pour fournir au client.
"""
info = loadFactureConfig(request)
contract = Contract.objects.get(pk=contractid)
# Create the HttpResponse object with the appropriate PDF headers.
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="facture_' + str(contract.reference) + '.pdf"'
document = MyDocument(response)
document.header(info, contract)
document.title(contract)
document.payementInformation(info, contract)
document.prestations(contract)
document.conclusion(info, contract)
document.addSignature()
document.download()
return response

View File

@ -0,0 +1,67 @@
# Generated by Django 4.0 on 2024-04-25 17:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='DescriptionType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Nom')),
('year', models.IntegerField(verbose_name='Année')),
('quotity', models.DecimalField(decimal_places=2, max_digits=3, verbose_name='Quotité')),
],
options={
'verbose_name': 'Catégorie',
'verbose_name_plural': 'Catégories',
},
),
migrations.CreateModel(
name='TvaType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.CharField(blank=True, max_length=255, verbose_name='Nom')),
('percent', models.DecimalField(decimal_places=3, max_digits=4, verbose_name='Pourcentage')),
('datebegin', models.DateField()),
('dateend', models.DateField(blank=True, null=True)),
],
options={
'verbose_name': 'Type de TVA',
'verbose_name_plural': 'Types de TVA',
},
),
migrations.CreateModel(
name='Transaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField()),
('information', models.CharField(max_length=255)),
('amountTva', models.DecimalField(blank=True, decimal_places=2, max_digits=5, verbose_name='Montant (TVAC)')),
('amountHTva', models.DecimalField(blank=True, decimal_places=2, max_digits=5, verbose_name='Montant (HTVA)')),
('amountDeductible', models.DecimalField(blank=True, decimal_places=2, max_digits=5, verbose_name='Montant Déductible')),
('paid', models.BooleanField(blank=True, default=False, verbose_name='Payé ?')),
('ticket', models.BooleanField(blank=True, default=False, verbose_name='Ticket ?')),
('notes', models.TextField(blank=True, null=True)),
('quotity', models.DecimalField(blank=True, decimal_places=2, max_digits=3, null=True, verbose_name='Quotité')),
('description', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='TransactionsDesc', to='compta.descriptiontype')),
],
options={
'verbose_name': 'Transaction',
'verbose_name_plural': 'Transactions',
},
),
migrations.AddField(
model_name='descriptiontype',
name='tva_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='get_descriptiontype', to='compta.tvatype'),
),
]

View File

View File

@ -103,7 +103,7 @@ USE_TZ = True
STATIC_URL = '/static/'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
os.path.join(BASE_DIR, 'static'),
)
# Default primary key field type

Binary file not shown.

10
static/css/a4_paper.css Normal file
View File

@ -0,0 +1,10 @@
@page {
/* A4 : 210mm x 297mm */
size: A4;
/* margin: 1.5cm 1.5cm 1.5cm 1.5cm; */
/* border: 1px solid red; */
}
/* @page :first {
margin: 0.8cm 1cm 1cm 1.3cm;
} */

File diff suppressed because it is too large Load Diff

67
static/css/to_pdf.css Normal file
View File

@ -0,0 +1,67 @@
* {
font-family: "Unbounded";
font-size: 12px;
/* DEBUG OPTION */
/* border: 1px solid black; */
}
.main_title {
margin-top: 10px;
margin-right: -4px;
font-size: 85px;
font-weight: bold;
}
.bordered_top {
border-top: 1px solid black;
}
.bordered_bottom {
border-bottom: 1px solid black;
}
.bordered_right {
border-right: 1px solid black;
}
.bordered-left {
border-left: 1px solid black;
}
td {
color: black !important;
}
.white-content .table>thead>tr>th, .white-content .table {
color: black !important;
}
.white-content .table>tbody>tr>td {
color: black !important;
}
.white-content .table>thead>tr>th {
border-color: black;
border-bottom: 1px solid black !important;
}
.white-content .table>tbody>tr>td {
border: none !important;
}
.white-content .table .bordered-row {
border-color: black;
border-bottom: 1px solid black !important;
}
.pdf_footer {
position: fixed;
bottom: 0cm;
width: 100%;
/* height: 5cm; */
}
/* DEBUG CLASSES */
.bordered {
border: 2px solid black;
}

BIN
static/img/apple-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
static/img/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 99 KiB

BIN
static/img/signature_gt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

BIN
static/img/visite2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -1,47 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div id="page" class=" sidebar_right">
<div class="container">
<div id="frame2">
<div id="content">
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<h1>{{ contract.name }} <small>({{ prestation_count }} prestations)</small></h1>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-10 col-lg-8 col-md-offset-1 col-lg-offset-2">
<table class="table table-striped table-bordered table-condensed">
<thead>
<th>Date</th>
<th>Prestation</th>
<th class="centered">Unité</th>
<th class="centered">Prix</th>
<th class="centered">Total</th>
</thead>
{% for prestation in prestation_list %}
<tr>
<td>{{ prestation.date|date:"d-m-Y" }}</td>
<td>{{ prestation.label }}</td>
<td class="push-right">{{ prestation.unit }}</td>
<td class="push-right">{{ prestation.unit_price }}</td>
<td class="push-right">{{ prestation.total_amount }}</td>
</tr>
{% endfor %}
<tr>
<td colspan="4" class="push-right"><b>Total :</b></td>
<td class="push-right"><b>{{ total }}</b></td>
</tr>
</table>
<p class="right"><a href="/billing/contract/pdf/{{ contract.id }}" class="btn btn-default btn-primary" role="button">Facture PDF</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}