Update billing system
This commit is contained in:
parent
6bd80f48e4
commit
30287e0ec0
|
@ -1,5 +1,13 @@
|
||||||
# Comptabilité d'indépendant complémentaire
|
# 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é
|
## Application de comptabilité
|
||||||
Elle permet de gérer sa comptabilité (mouvement d'argent) d'indépendant complémentaire.
|
Elle permet de gérer sa comptabilité (mouvement d'argent) d'indépendant complémentaire.
|
||||||
|
|
||||||
|
|
|
@ -3,32 +3,40 @@ from django.contrib import admin
|
||||||
from .models import (
|
from .models import (
|
||||||
Client,
|
Client,
|
||||||
Contract,
|
Contract,
|
||||||
|
DiscountContract,
|
||||||
Prestation,
|
Prestation,
|
||||||
|
PrestationType,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@admin.register(Client)
|
||||||
class ClientAdmin(admin.ModelAdmin):
|
class ClientAdmin(admin.ModelAdmin):
|
||||||
model = Client
|
model = Client
|
||||||
|
|
||||||
list_display = ('name', 'address', 'postal_code', 'city', 'contact')
|
list_display = ('name', 'address', 'postal_code', 'city', 'contact')
|
||||||
search_fields = ('name', 'adress', 'city')
|
search_fields = ('name', 'adress', 'city')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Contract)
|
||||||
class ContractAdmin(admin.ModelAdmin):
|
class ContractAdmin(admin.ModelAdmin):
|
||||||
model = Contract
|
model = Contract
|
||||||
|
|
||||||
list_display = ('name', 'client', 'advance', 'reference', 'date')
|
list_display = ('name', 'client', 'advance', 'reference', 'date')
|
||||||
search_fields = ('name', )
|
search_fields = ('name', )
|
||||||
list_filter = ('is_finished', ) # 'date__year',
|
list_filter = ('is_finished', ) # 'date__year',
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Prestation)
|
||||||
class PrestationAdmin(admin.ModelAdmin):
|
class PrestationAdmin(admin.ModelAdmin):
|
||||||
model = Prestation
|
model = Prestation
|
||||||
|
list_display = ('date', 'label', 'total_amount_htva', 'total_amount_tvac', 'contract')
|
||||||
list_display = ('date', 'label', 'total_amount', 'contract')
|
|
||||||
search_fields = ('label', )
|
search_fields = ('label', )
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Client, ClientAdmin)
|
@admin.register(PrestationType)
|
||||||
admin.site.register(Contract, ContractAdmin)
|
class PrestationTypeAdmin(admin.ModelAdmin):
|
||||||
admin.site.register(Prestation, PrestationAdmin)
|
model = PrestationType
|
||||||
|
list_display = ('label',)
|
||||||
|
search_fields = ('label',)
|
||||||
|
|
||||||
|
@admin.register(DiscountContract)
|
||||||
|
class DiscountContractAdmin(admin.ModelAdmin):
|
||||||
|
model = DiscountContract
|
||||||
|
list_display = ('contract', 'label', 'quantity', 'unit')
|
||||||
|
|
|
@ -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", }
|
||||||
|
# )
|
||||||
|
# }
|
|
@ -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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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)'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,6 +1,13 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import Sum
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
|
DISCOUNT_UNIT_CHOICE = [
|
||||||
|
(0, "%"),
|
||||||
|
(1, "€"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class Client(models.Model):
|
class Client(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -12,8 +19,59 @@ class Client(models.Model):
|
||||||
postal_code = models.IntegerField(verbose_name="Code postal")
|
postal_code = models.IntegerField(verbose_name="Code postal")
|
||||||
city = models.CharField(max_length=255, verbose_name="Ville")
|
city = models.CharField(max_length=255, verbose_name="Ville")
|
||||||
contact = models.CharField(max_length=255, verbose_name="Personne de contact")
|
contact = models.CharField(max_length=255, verbose_name="Personne de contact")
|
||||||
company_number = models.CharField(max_length=50, verbose_name="N° d'entreprise",
|
email = models.EmailField(max_length=254)
|
||||||
blank=True, null=True)
|
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):
|
def __str__(self):
|
||||||
return "%s" % (self.name)
|
return "%s" % (self.name)
|
||||||
|
@ -25,16 +83,25 @@ class Contract(models.Model):
|
||||||
verbose_name_plural = 'Contrats'
|
verbose_name_plural = 'Contrats'
|
||||||
|
|
||||||
name = models.CharField(max_length=255, verbose_name="Nom")
|
name = models.CharField(max_length=255, verbose_name="Nom")
|
||||||
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name="get_contract")
|
client = models.ForeignKey(Client, on_delete=models.CASCADE, related_name="contracts")
|
||||||
advance = models.DecimalField(max_digits=6, decimal_places=2, blank=True,
|
advance = models.DecimalField(
|
||||||
verbose_name="Acompte", default=0)
|
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)
|
reference = models.CharField(max_length=255, verbose_name="Référence", blank=True, null=True)
|
||||||
is_finished = models.BooleanField(default=True, blank=True)
|
is_finished = models.BooleanField(default=True, blank=True)
|
||||||
date = models.DateField(auto_now_add=True)
|
date = models.DateField(auto_now_add=True)
|
||||||
|
invoiced_date = models.DateField(default=None, blank=True, null=True)
|
||||||
|
|
||||||
def __str__(self):
|
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):
|
# def __generate_reference(self):
|
||||||
# """
|
# """
|
||||||
# Génère automatiquement la référence du contract
|
# Génère automatiquement la référence du contract
|
||||||
|
@ -55,6 +122,15 @@ class Contract(models.Model):
|
||||||
# self.__generate_reference()
|
# self.__generate_reference()
|
||||||
# super().save(*args, **kwargs)
|
# 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 Prestation(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -62,21 +138,59 @@ class Prestation(models.Model):
|
||||||
verbose_name_plural = 'Prestations'
|
verbose_name_plural = 'Prestations'
|
||||||
ordering = ['date']
|
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()
|
date = models.DateField()
|
||||||
label = models.CharField(max_length=255, verbose_name='Libellé')
|
label = models.CharField(max_length=255, verbose_name='Libellé')
|
||||||
unit = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Unité",
|
unit = models.DecimalField(
|
||||||
default=1)
|
max_digits=5,
|
||||||
unit_price = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Prix unitaire",
|
decimal_places=2,
|
||||||
default="12,5")
|
verbose_name="Unité",
|
||||||
total_amount = models.DecimalField(max_digits=6, decimal_places=2, blank=True,
|
default=1
|
||||||
verbose_name="Prix", default=0)
|
)
|
||||||
|
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):
|
def __compute_total_amount(self):
|
||||||
"""
|
"""
|
||||||
Calcule le montant total de la prestation.
|
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):
|
def save(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -84,3 +198,40 @@ class Prestation(models.Model):
|
||||||
"""
|
"""
|
||||||
self.__compute_total_amount()
|
self.__compute_total_amount()
|
||||||
super().save(*args, **kwargs)
|
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)
|
||||||
|
|
|
@ -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 %} <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 %} <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 %} <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 %}
|
|
@ -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 %}
|
|
@ -15,6 +15,7 @@
|
||||||
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
|
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
|
||||||
<table class="table table-striped table-bordered table-condensed">
|
<table class="table table-striped table-bordered table-condensed">
|
||||||
<thead>
|
<thead>
|
||||||
|
<th></th>
|
||||||
<th>Client</th>
|
<th>Client</th>
|
||||||
<th>Contact</th>
|
<th>Contact</th>
|
||||||
<th>Adresse</th>
|
<th>Adresse</th>
|
||||||
|
@ -23,7 +24,8 @@
|
||||||
</thead>
|
</thead>
|
||||||
{% for client in client_list %}
|
{% for client in client_list %}
|
||||||
<tr>
|
<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.contact }}</td>
|
||||||
<td>{{ client.address }}</td>
|
<td>{{ client.address }}</td>
|
||||||
<td>{{ client.postal_code }}</td>
|
<td>{{ client.postal_code }}</td>
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -8,16 +8,19 @@
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
|
<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>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-sm-12 col-md-12 col-lg-12">
|
<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">
|
<table class="table table-striped table-bordered table-condensed">
|
||||||
<thead>
|
<thead>
|
||||||
<th class="centered">Année</th>
|
<th class="centered">Année</th>
|
||||||
<th>Nom</th>
|
<th>Nom</th>
|
||||||
|
{% if not client %}
|
||||||
<th class="centered">Client</th>
|
<th class="centered">Client</th>
|
||||||
|
{% endif %}
|
||||||
<th class="centered">Acompte</th>
|
<th class="centered">Acompte</th>
|
||||||
<th class="centered"># Prest.</th>
|
<th class="centered"># Prest.</th>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -25,12 +28,15 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td class="centered">{{ contract.date.year }}</td>
|
<td class="centered">{{ contract.date.year }}</td>
|
||||||
<td><a href="{% url 'contract_detail' contract.id %}">{{ contract.name }}</a></td>
|
<td><a href="{% url 'contract_detail' contract.id %}">{{ contract.name }}</a></td>
|
||||||
|
{% if not client %}
|
||||||
<td>{{ contract.client }}</td>
|
<td>{{ contract.client }}</td>
|
||||||
|
{% endif %}
|
||||||
<td class="push-right">{{ contract.advance }}</td>
|
<td class="push-right">{{ contract.advance }}</td>
|
||||||
<td class="push-right">{{ contract.get_prestation.count }}</td>
|
<td class="push-right">{{ contract.get_prestation.count }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -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 %} <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 %} <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 %} <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 %} <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 %} <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 %}
|
|
@ -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>
|
|
@ -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>
|
|
@ -3,8 +3,9 @@ from django.urls import path, re_path
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
billing_urlpatterns = [
|
billing_urlpatterns = [
|
||||||
path(r"contract/pdf/<int:contractid>/", views.contract_export, name="contract_export"),
|
path(r"contract/pdf/<int:contract_id>/", views.contract_export, name="contract_export"),
|
||||||
path(r"contract/<int:contractid>/", views.contract_detail, name="contract_detail"),
|
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"contract", views.contract_listing, name="contract_listing"),
|
||||||
path(r"prestation", views.prestation_listing, name="prestation_listing"),
|
path(r"prestation", views.prestation_listing, name="prestation_listing"),
|
||||||
path(r"client", views.client_listing, name="client_listing"),
|
path(r"client", views.client_listing, name="client_listing"),
|
||||||
|
|
317
billing/views.py
317
billing/views.py
|
@ -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.template import RequestContext
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.db.models import Sum
|
|
||||||
|
|
||||||
from reportlab.pdfgen import canvas
|
from django.template.loader import render_to_string
|
||||||
from reportlab.lib.colors import Color, black, blue, red
|
|
||||||
from reportlab.lib.pagesizes import A4
|
from weasyprint import HTML, CSS
|
||||||
|
# from weasyprint.fonts import FontConfiguration
|
||||||
|
|
||||||
# from PIL import Image
|
# from PIL import Image
|
||||||
import os
|
import os
|
||||||
|
import pendulum
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from datetime import date
|
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.
|
Renvoie la liste de tous les contrats.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
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()
|
contract_list = Contract.objects.all()
|
||||||
context = {'contract_list': contract_list}
|
|
||||||
return render(request, 'billing/contract/listing.html', context)
|
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
|
Renvoie toutes les informations relatives à un contrat, en ce y compris les prestations
|
||||||
relatives à celui-ci.
|
relatives à celui-ci.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
contract = Contract.objects.get(pk=contractid)
|
contract = Contract.objects.get(pk=contract_id)
|
||||||
prestation_list = contract.get_prestation.all()
|
total_without_discount_htva = contract.get_contract_sum("total_amount_htva")
|
||||||
prestation_count = prestation_list.count()
|
total_without_discount_tvac = contract.get_contract_sum("total_amount_tvac")
|
||||||
total = list(contract.get_prestation.all().aggregate(Sum('total_amount')).values())[0]
|
|
||||||
context = {'contract': contract,
|
total_with_discount_htva = total_without_discount_htva
|
||||||
'prestation_list': prestation_list,
|
total_with_discount_tvac = total_without_discount_tvac
|
||||||
'prestation_count': prestation_count,
|
for discount in contract.discounts.all():
|
||||||
'total': total}
|
total_with_discount_htva -= discount.amount_htva()
|
||||||
return render(request, 'billing/contract/detail.html', context)
|
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):
|
def client_listing(request):
|
||||||
|
@ -54,8 +71,8 @@ def client_listing(request):
|
||||||
Renvoie la liste de tous les clients.
|
Renvoie la liste de tous les clients.
|
||||||
"""
|
"""
|
||||||
client_list = Client.objects.all()
|
client_list = Client.objects.all()
|
||||||
context = {'client_list': client_list}
|
context = {"client_list": client_list}
|
||||||
return render(request, 'billing/client/listing.html', context)
|
return render(request, "client/listing.html", context)
|
||||||
|
|
||||||
|
|
||||||
def prestation_listing(request):
|
def prestation_listing(request):
|
||||||
|
@ -63,229 +80,59 @@ def prestation_listing(request):
|
||||||
Renvoie la liste de toutes les prestations.
|
Renvoie la liste de toutes les prestations.
|
||||||
"""
|
"""
|
||||||
prestation_list = Prestation.objects.all()
|
prestation_list = Prestation.objects.all()
|
||||||
context = {'prestation_list': prestation_list}
|
context = {"prestation_list": prestation_list}
|
||||||
return render(request, 'billing/prestation/listing.html', context)
|
return render(request, "prestation/listing.html", context)
|
||||||
|
|
||||||
|
|
||||||
Y = 841.89
|
def contract_export(request, contract_id):
|
||||||
X = 35
|
"""Génere un fichier PDF à fournir au client."""
|
||||||
RIGHT_X = 595.27 - X
|
contract = get_object_or_404(Contract, pk=contract_id)
|
||||||
TITLED_X = 125
|
|
||||||
INDENTED_X = X + 5
|
|
||||||
INDENTED_RIGHT_X = RIGHT_X - 5
|
|
||||||
|
|
||||||
PRESTATION_COLUMN_2 = INDENTED_X + 65
|
if not contract.invoiced_date:
|
||||||
PRESTATION_COLUMN_3 = INDENTED_X + 400
|
today = pendulum.now().date()
|
||||||
PRESTATION_COLUMN_4 = INDENTED_X + 455
|
contract.invoiced_date = today
|
||||||
|
contract.save()
|
||||||
|
|
||||||
COMMON_LINE_HEIGHT = -15
|
contract = Contract.objects.get(pk=contract_id)
|
||||||
DOUBLE_LINE_HEIGHT = COMMON_LINE_HEIGHT * 2
|
total_without_discount_htva = contract.get_contract_sum("total_amount_htva")
|
||||||
BIG_LINE_HEIGHT = COMMON_LINE_HEIGHT * 3
|
total_without_discount_tvac = contract.get_contract_sum("total_amount_tvac")
|
||||||
HUGE_LINE_HEIGHT = COMMON_LINE_HEIGHT * 4
|
|
||||||
|
|
||||||
class MyDocument(object):
|
total_with_discount_htva = total_without_discount_htva
|
||||||
# Create the PDF object, using the response object as its "file."
|
total_with_discount_tvac = total_without_discount_tvac
|
||||||
# http://www.reportlab.com/docs/reportlab-userguide.pdf
|
for discount in contract.discounts.all():
|
||||||
# canvas.rect(x, y, width, height, stroke=1, fill=0)
|
total_with_discount_htva -= discount.amount_htva()
|
||||||
# localhost:8000/billing/contract/pdf/2
|
total_with_discount_tvac -= discount.amount_tvac()
|
||||||
|
|
||||||
def __init__(self, response):
|
context = {
|
||||||
# Create the PDF object, using the response object as its "file."
|
"contract": contract,
|
||||||
self.document = canvas.Canvas(response, pagesize=A4)
|
"client": contract.client,
|
||||||
self.y = Y - X
|
"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):
|
# return render(request, "to_pdf/facture.html", context)
|
||||||
"""
|
|
||||||
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
|
|
||||||
|
|
||||||
# if y < 120;
|
html = render_to_string("to_pdf/facture.html", context)
|
||||||
# document.PageBreak()
|
response = HttpResponse(content_type="application/pdf")
|
||||||
# y = 790
|
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_config = FontConfiguration()
|
||||||
font = font_family
|
HTML(string=html, base_url=request.build_absolute_uri()).write_pdf(
|
||||||
if font_decoration is not None:
|
response,
|
||||||
font += "-" + font_decoration
|
stylesheets=[
|
||||||
self.document.setFont(font, font_size)
|
CSS(settings.STATICFILES_DIRS[0] + "/css/a4_paper.css"),
|
||||||
self.document.drawString(x, self.y, string)
|
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
|
return response
|
||||||
|
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
|
@ -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
|
@ -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;
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
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 |
Binary file not shown.
After Width: | Height: | Size: 100 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.6 KiB |
|
@ -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 %}
|
|
Loading…
Reference in New Issue