mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-11-23 11:53:12 +00:00
Merge branch 'feature_improve_invoice' into 'dev'
Feature improve invoice See merge request federez/re2o!382
This commit is contained in:
commit
125c4244bc
15 changed files with 734 additions and 118 deletions
|
@ -30,7 +30,7 @@ from django.contrib import admin
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
|
|
||||||
from .models import Facture, Article, Banque, Paiement, Cotisation, Vente
|
from .models import Facture, Article, Banque, Paiement, Cotisation, Vente
|
||||||
from .models import CustomInvoice
|
from .models import CustomInvoice, CostEstimate
|
||||||
|
|
||||||
|
|
||||||
class FactureAdmin(VersionAdmin):
|
class FactureAdmin(VersionAdmin):
|
||||||
|
@ -38,6 +38,11 @@ class FactureAdmin(VersionAdmin):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CostEstimateAdmin(VersionAdmin):
|
||||||
|
"""Admin class for cost estimates."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class CustomInvoiceAdmin(VersionAdmin):
|
class CustomInvoiceAdmin(VersionAdmin):
|
||||||
"""Admin class for custom invoices."""
|
"""Admin class for custom invoices."""
|
||||||
pass
|
pass
|
||||||
|
@ -76,3 +81,4 @@ admin.site.register(Paiement, PaiementAdmin)
|
||||||
admin.site.register(Vente, VenteAdmin)
|
admin.site.register(Vente, VenteAdmin)
|
||||||
admin.site.register(Cotisation, CotisationAdmin)
|
admin.site.register(Cotisation, CotisationAdmin)
|
||||||
admin.site.register(CustomInvoice, CustomInvoiceAdmin)
|
admin.site.register(CustomInvoice, CustomInvoiceAdmin)
|
||||||
|
admin.site.register(CostEstimate, CostEstimateAdmin)
|
||||||
|
|
|
@ -46,7 +46,10 @@ from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from re2o.field_permissions import FieldPermissionFormMixin
|
from re2o.field_permissions import FieldPermissionFormMixin
|
||||||
from re2o.mixins import FormRevMixin
|
from re2o.mixins import FormRevMixin
|
||||||
from .models import Article, Paiement, Facture, Banque, CustomInvoice
|
from .models import (
|
||||||
|
Article, Paiement, Facture, Banque,
|
||||||
|
CustomInvoice, Vente, CostEstimate
|
||||||
|
)
|
||||||
from .payment_methods import balance
|
from .payment_methods import balance
|
||||||
|
|
||||||
|
|
||||||
|
@ -104,7 +107,44 @@ class SelectArticleForm(FormRevMixin, Form):
|
||||||
user = kwargs.pop('user')
|
user = kwargs.pop('user')
|
||||||
target_user = kwargs.pop('target_user', None)
|
target_user = kwargs.pop('target_user', None)
|
||||||
super(SelectArticleForm, self).__init__(*args, **kwargs)
|
super(SelectArticleForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['article'].queryset = Article.find_allowed_articles(user, target_user)
|
self.fields['article'].queryset = Article.find_allowed_articles(
|
||||||
|
user, target_user)
|
||||||
|
|
||||||
|
|
||||||
|
class DiscountForm(Form):
|
||||||
|
"""
|
||||||
|
Form used in oder to create a discount on an invoice.
|
||||||
|
"""
|
||||||
|
is_relative = forms.BooleanField(
|
||||||
|
label=_("Discount is on percentage"),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
discount = forms.DecimalField(
|
||||||
|
label=_("Discount"),
|
||||||
|
max_value=100,
|
||||||
|
min_value=0,
|
||||||
|
max_digits=5,
|
||||||
|
decimal_places=2,
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply_to_invoice(self, invoice):
|
||||||
|
invoice_price = invoice.prix_total()
|
||||||
|
discount = self.cleaned_data['discount']
|
||||||
|
is_relative = self.cleaned_data['is_relative']
|
||||||
|
if is_relative:
|
||||||
|
amount = discount/100 * invoice_price
|
||||||
|
else:
|
||||||
|
amount = discount
|
||||||
|
if amount:
|
||||||
|
name = _("{}% discount") if is_relative else _("{}€ discount")
|
||||||
|
name = name.format(discount)
|
||||||
|
Vente.objects.create(
|
||||||
|
facture=invoice,
|
||||||
|
name=name,
|
||||||
|
prix=-amount,
|
||||||
|
number=1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CustomInvoiceForm(FormRevMixin, ModelForm):
|
class CustomInvoiceForm(FormRevMixin, ModelForm):
|
||||||
|
@ -116,6 +156,15 @@ class CustomInvoiceForm(FormRevMixin, ModelForm):
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class CostEstimateForm(FormRevMixin, ModelForm):
|
||||||
|
"""
|
||||||
|
Form used to create a cost estimate.
|
||||||
|
"""
|
||||||
|
class Meta:
|
||||||
|
model = CostEstimate
|
||||||
|
exclude = ['paid', 'final_invoice']
|
||||||
|
|
||||||
|
|
||||||
class ArticleForm(FormRevMixin, ModelForm):
|
class ArticleForm(FormRevMixin, ModelForm):
|
||||||
"""
|
"""
|
||||||
Form used to create an article.
|
Form used to create an article.
|
||||||
|
@ -248,7 +297,8 @@ class RechargeForm(FormRevMixin, Form):
|
||||||
super(RechargeForm, self).__init__(*args, **kwargs)
|
super(RechargeForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['payment'].empty_label = \
|
self.fields['payment'].empty_label = \
|
||||||
_("Select a payment method")
|
_("Select a payment method")
|
||||||
self.fields['payment'].queryset = Paiement.find_allowed_payments(user_source).exclude(is_balance=True)
|
self.fields['payment'].queryset = Paiement.find_allowed_payments(
|
||||||
|
user_source).exclude(is_balance=True)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""
|
"""
|
||||||
|
@ -266,4 +316,3 @@ class RechargeForm(FormRevMixin, Form):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return self.cleaned_data
|
return self.cleaned_data
|
||||||
|
|
||||||
|
|
20
cotisations/migrations/0036_custominvoice_remark.py
Normal file
20
cotisations/migrations/0036_custominvoice_remark.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2018-12-29 14:22
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cotisations', '0035_notepayment'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='custominvoice',
|
||||||
|
name='remark',
|
||||||
|
field=models.TextField(blank=True, null=True, verbose_name='Remark'),
|
||||||
|
),
|
||||||
|
]
|
28
cotisations/migrations/0037_costestimate.py
Normal file
28
cotisations/migrations/0037_costestimate.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2018-12-29 21:03
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cotisations', '0036_custominvoice_remark'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CostEstimate',
|
||||||
|
fields=[
|
||||||
|
('custominvoice_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cotisations.CustomInvoice')),
|
||||||
|
('validity', models.DurationField(verbose_name='Period of validity')),
|
||||||
|
('final_invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='origin_cost_estimate', to='cotisations.CustomInvoice')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'permissions': (('view_costestimate', 'Can view a cost estimate object'),),
|
||||||
|
},
|
||||||
|
bases=('cotisations.custominvoice',),
|
||||||
|
),
|
||||||
|
]
|
31
cotisations/migrations/0038_auto_20181231_1657.py
Normal file
31
cotisations/migrations/0038_auto_20181231_1657.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2018-12-31 22:57
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('cotisations', '0037_costestimate'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='costestimate',
|
||||||
|
name='final_invoice',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='origin_cost_estimate', to='cotisations.CustomInvoice'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='costestimate',
|
||||||
|
name='validity',
|
||||||
|
field=models.DurationField(help_text='DD HH:MM:SS', verbose_name='Period of validity'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='custominvoice',
|
||||||
|
name='paid',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Paid'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -284,8 +284,65 @@ class CustomInvoice(BaseInvoice):
|
||||||
verbose_name=_("Address")
|
verbose_name=_("Address")
|
||||||
)
|
)
|
||||||
paid = models.BooleanField(
|
paid = models.BooleanField(
|
||||||
verbose_name=_("Paid")
|
verbose_name=_("Paid"),
|
||||||
|
default=False
|
||||||
)
|
)
|
||||||
|
remark = models.TextField(
|
||||||
|
verbose_name=_("Remark"),
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CostEstimate(CustomInvoice):
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('view_costestimate', _("Can view a cost estimate object")),
|
||||||
|
)
|
||||||
|
validity = models.DurationField(
|
||||||
|
verbose_name=_("Period of validity"),
|
||||||
|
help_text="DD HH:MM:SS"
|
||||||
|
)
|
||||||
|
final_invoice = models.ForeignKey(
|
||||||
|
CustomInvoice,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="origin_cost_estimate",
|
||||||
|
primary_key=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_invoice(self):
|
||||||
|
"""Create a CustomInvoice from the CostEstimate."""
|
||||||
|
if self.final_invoice is not None:
|
||||||
|
return self.final_invoice
|
||||||
|
invoice = CustomInvoice()
|
||||||
|
invoice.recipient = self.recipient
|
||||||
|
invoice.payment = self.payment
|
||||||
|
invoice.address = self.address
|
||||||
|
invoice.paid = False
|
||||||
|
invoice.remark = self.remark
|
||||||
|
invoice.date = timezone.now()
|
||||||
|
invoice.save()
|
||||||
|
self.final_invoice = invoice
|
||||||
|
self.save()
|
||||||
|
for sale in self.vente_set.all():
|
||||||
|
Vente.objects.create(
|
||||||
|
facture=invoice,
|
||||||
|
name=sale.name,
|
||||||
|
prix=sale.prix,
|
||||||
|
number=sale.number,
|
||||||
|
)
|
||||||
|
return invoice
|
||||||
|
|
||||||
|
def can_delete(self, user_request, *args, **kwargs):
|
||||||
|
if not user_request.has_perm('cotisations.delete_costestimate'):
|
||||||
|
return False, _("You don't have the right "
|
||||||
|
"to delete a cost estimate.")
|
||||||
|
if self.final_invoice is not None:
|
||||||
|
return False, _("The cost estimate has an "
|
||||||
|
"invoice and cannot be deleted.")
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
# TODO : change Vente to Purchase
|
# TODO : change Vente to Purchase
|
||||||
|
|
101
cotisations/templates/cotisations/aff_cost_estimate.html
Normal file
101
cotisations/templates/cotisations/aff_cost_estimate.html
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
{% comment %}
|
||||||
|
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
||||||
|
se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
|
quelques clics.
|
||||||
|
|
||||||
|
Copyright © 2018 Hugo Levy-Falk
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
{% endcomment %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load acl %}
|
||||||
|
{% load logs_extra %}
|
||||||
|
{% load design %}
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
{% if cost_estimate_list.paginator %}
|
||||||
|
{% include 'pagination.html' with list=cost_estimate_list%}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
{% trans "Recipient" as tr_recip %}
|
||||||
|
{% include 'buttons/sort.html' with prefix='invoice' col='user' text=tr_user %}
|
||||||
|
</th>
|
||||||
|
<th>{% trans "Designation" %}</th>
|
||||||
|
<th>{% trans "Total price" %}</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Payment method" as tr_payment_method %}
|
||||||
|
{% include 'buttons/sort.html' with prefix='invoice' col='payement' text=tr_payment_method %}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Date" as tr_date %}
|
||||||
|
{% include 'buttons/sort.html' with prefix='invoice' col='date' text=tr_date %}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Validity" as tr_validity %}
|
||||||
|
{% include 'buttons/sort.html' with prefix='invoice' col='validity' text=tr_validity %}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Cost estimate ID" as tr_estimate_id %}
|
||||||
|
{% include 'buttons/sort.html' with prefix='invoice' col='id' text=tr_estimate_id %}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{% trans "Invoice created" as tr_invoice_created%}
|
||||||
|
{% include 'buttons/sort.html' with prefix='invoice' col='paid' text=tr_invoice_created %}
|
||||||
|
</th>
|
||||||
|
<th></th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{% for estimate in cost_estimate_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ estimate.recipient }}</td>
|
||||||
|
<td>{{ estimate.name }}</td>
|
||||||
|
<td>{{ estimate.prix_total }}</td>
|
||||||
|
<td>{{ estimate.payment }}</td>
|
||||||
|
<td>{{ estimate.date }}</td>
|
||||||
|
<td>{{ estimate.validity }}</td>
|
||||||
|
<td>{{ estimate.id }}</td>
|
||||||
|
<td>
|
||||||
|
{% if estimate.final_invoice %}
|
||||||
|
<a href="{% url 'cotisations:edit-custom-invoice' estimate.final_invoice.pk %}"><i style="color: #1ECA18;" class="fa fa-check"></i></a>
|
||||||
|
{% else %}
|
||||||
|
<i style="color: #D10115;" class="fa fa-times"></i>'
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% can_edit estimate %}
|
||||||
|
{% include 'buttons/edit.html' with href='cotisations:edit-cost-estimate' id=estimate.id %}
|
||||||
|
{% acl_end %}
|
||||||
|
{% history_button estimate %}
|
||||||
|
{% include 'buttons/suppr.html' with href='cotisations:del-cost-estimate' id=estimate.id %}
|
||||||
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:cost-estimate-to-invoice' estimate.id %}">
|
||||||
|
<i class="fa fa-file"></i>
|
||||||
|
</a>
|
||||||
|
<a class="btn btn-primary btn-sm" role="button" href="{% url 'cotisations:cost-estimate-pdf' estimate.id %}">
|
||||||
|
<i class="fa fa-file-pdf-o"></i> {% trans "PDF" %}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if custom_invoice_list.paginator %}
|
||||||
|
{% include 'pagination.html' with list=custom_invoice_list %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
|
@ -35,7 +35,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
|
||||||
<form class="form" method="post">
|
<form class="form" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{% if title %}
|
||||||
|
<h3>{{title}}</h3>
|
||||||
|
{% else %}
|
||||||
<h3>{% trans "Edit the invoice" %}</h3>
|
<h3>{% trans "Edit the invoice" %}</h3>
|
||||||
|
{% endif %}
|
||||||
{% massive_bootstrap_form factureform 'user' %}
|
{% massive_bootstrap_form factureform 'user' %}
|
||||||
{{ venteform.management_form }}
|
{{ venteform.management_form }}
|
||||||
<h3>{% trans "Articles" %}</h3>
|
<h3>{% trans "Articles" %}</h3>
|
||||||
|
|
|
@ -44,6 +44,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% blocktrans %}Current balance: {{ balance }} €{% endblocktrans %}
|
{% blocktrans %}Current balance: {{ balance }} €{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% bootstrap_form_errors factureform %}
|
||||||
|
{% bootstrap_form_errors discount_form %}
|
||||||
|
|
||||||
<form class="form" method="post">
|
<form class="form" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
@ -68,8 +70,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<input class="btn btn-primary btn-block" role="button" value="{% trans "Add an extra article"%}" id="add_one">
|
<input class="btn btn-primary btn-block" role="button" value="{% trans "Add an extra article"%}" id="add_one">
|
||||||
|
<h3>{% trans "Discount" %}</h3>
|
||||||
|
{% if discount_form %}
|
||||||
|
{% bootstrap_form discount_form %}
|
||||||
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans %}Total price: <span id="total_price">0,00</span> €{% endblocktrans %}
|
{% blocktrans %}Total price: <span id="total_price">0,00</span> €{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% bootstrap_button action_name button_type='submit' icon='ok' button_class='btn-success' %}
|
{% bootstrap_button action_name button_type='submit' icon='ok' button_class='btn-success' %}
|
||||||
|
@ -78,105 +84,117 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
{% if articlesformset or payment_method%}
|
{% if articlesformset or payment_method%}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
{% if articlesformset %}
|
{% if articlesformset %}
|
||||||
var prices = {};
|
var prices = {};
|
||||||
{% for article in articlelist %}
|
{% for article in articlelist %}
|
||||||
prices[{{ article.id|escapejs }}] = {{ article.prix }};
|
prices[{{ article.id|escapejs }}] = {{ article.prix }};
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
var template = `Article :
|
var template = `Article :
|
||||||
{% bootstrap_form articlesformset.empty_form label_class='sr-only' %}
|
{% bootstrap_form articlesformset.empty_form label_class='sr-only' %}
|
||||||
|
|
||||||
<button class="btn btn-danger btn-sm"
|
<button class="btn btn-danger btn-sm"
|
||||||
id="id_form-__prefix__-article-remove" type="button">
|
id="id_form-__prefix__-article-remove" type="button">
|
||||||
<span class="fa fa-times"></span>
|
<span class="fa fa-times"></span>
|
||||||
</button>`
|
</button>`
|
||||||
|
|
||||||
function add_article(){
|
function add_article(){
|
||||||
// Index start at 0 => new_index = number of items
|
// Index start at 0 => new_index = number of items
|
||||||
var new_index =
|
var new_index =
|
||||||
document.getElementsByClassName('product_to_sell').length;
|
document.getElementsByClassName('product_to_sell').length;
|
||||||
document.getElementById('id_form-TOTAL_FORMS').value ++;
|
document.getElementById('id_form-TOTAL_FORMS').value ++;
|
||||||
var new_article = document.createElement('div');
|
var new_article = document.createElement('div');
|
||||||
new_article.className = 'product_to_sell form-inline';
|
new_article.className = 'product_to_sell form-inline';
|
||||||
new_article.innerHTML = template.replace(/__prefix__/g, new_index);
|
new_article.innerHTML = template.replace(/__prefix__/g, new_index);
|
||||||
document.getElementById('form_set').appendChild(new_article);
|
document.getElementById('form_set').appendChild(new_article);
|
||||||
add_listenner_for_id(new_index);
|
add_listenner_for_id(new_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_price(){
|
function update_price(){
|
||||||
var price = 0;
|
var price = 0;
|
||||||
var product_count =
|
var product_count =
|
||||||
document.getElementsByClassName('product_to_sell').length;
|
document.getElementsByClassName('product_to_sell').length;
|
||||||
var article, article_price, quantity;
|
var article, article_price, quantity;
|
||||||
for (i = 0; i < product_count; ++i){
|
for (i = 0; i < product_count; ++i){
|
||||||
article = document.getElementById(
|
article = document.getElementById(
|
||||||
'id_form-' + i.toString() + '-article').value;
|
'id_form-' + i.toString() + '-article').value;
|
||||||
if (article == '') {
|
if (article == '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
article_price = prices[article];
|
article_price = prices[article];
|
||||||
quantity = document.getElementById(
|
quantity = document.getElementById(
|
||||||
'id_form-' + i.toString() + '-quantity').value;
|
'id_form-' + i.toString() + '-quantity').value;
|
||||||
price += article_price * quantity;
|
price += article_price * quantity;
|
||||||
}
|
|
||||||
document.getElementById('total_price').innerHTML =
|
|
||||||
price.toFixed(2).toString().replace('.', ',');
|
|
||||||
}
|
}
|
||||||
|
{% if discount_form %}
|
||||||
function add_listenner_for_id(i){
|
var relative_discount = document.getElementById('id_is_relative').checked;
|
||||||
document.getElementById('id_form-' + i.toString() + '-article')
|
var discount = document.getElementById('id_discount').value;
|
||||||
.addEventListener("change", update_price, true);
|
if(relative_discount) {
|
||||||
document.getElementById('id_form-' + i.toString() + '-article')
|
discount = discount/100 * price;
|
||||||
.addEventListener("onkeypress", update_price, true);
|
|
||||||
document.getElementById('id_form-' + i.toString() + '-quantity')
|
|
||||||
.addEventListener("change", update_price, true);
|
|
||||||
document.getElementById('id_form-' + i.toString() + '-article-remove')
|
|
||||||
.addEventListener("click", function(event) {
|
|
||||||
var article = event.target.parentNode;
|
|
||||||
article.parentNode.removeChild(article);
|
|
||||||
document.getElementById('id_form-TOTAL_FORMS').value --;
|
|
||||||
update_price();
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
price -= discount;
|
||||||
|
{% endif %}
|
||||||
|
document.getElementById('total_price').innerHTML =
|
||||||
|
price.toFixed(2).toString().replace('.', ',');
|
||||||
|
}
|
||||||
|
|
||||||
// Add events manager when DOM is fully loaded
|
function add_listenner_for_id(i){
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.getElementById('id_form-' + i.toString() + '-article')
|
||||||
document.getElementById("add_one")
|
.addEventListener("change", update_price, true);
|
||||||
.addEventListener("click", add_article, true);
|
document.getElementById('id_form-' + i.toString() + '-article')
|
||||||
var product_count =
|
.addEventListener("onkeypress", update_price, true);
|
||||||
document.getElementsByClassName('product_to_sell').length;
|
document.getElementById('id_form-' + i.toString() + '-quantity')
|
||||||
for (i = 0; i < product_count; ++i){
|
.addEventListener("change", update_price, true);
|
||||||
add_listenner_for_id(i);
|
document.getElementById('id_form-' + i.toString() + '-article-remove')
|
||||||
}
|
.addEventListener("click", function(event) {
|
||||||
update_price();
|
var article = event.target.parentNode;
|
||||||
});
|
article.parentNode.removeChild(article);
|
||||||
|
document.getElementById('id_form-TOTAL_FORMS').value --;
|
||||||
|
update_price();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add events manager when DOM is fully loaded
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
document.getElementById("add_one")
|
||||||
|
.addEventListener("click", add_article, true);
|
||||||
|
var product_count =
|
||||||
|
document.getElementsByClassName('product_to_sell').length;
|
||||||
|
for (i = 0; i < product_count; ++i){
|
||||||
|
add_listenner_for_id(i);
|
||||||
|
}
|
||||||
|
document.getElementById('id_discount')
|
||||||
|
.addEventListener('change', update_price, true);
|
||||||
|
document.getElementById('id_is_relative')
|
||||||
|
.addEventListener('click', update_price, true);
|
||||||
|
update_price();
|
||||||
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if payment_method.templates %}
|
{% if payment_method.templates %}
|
||||||
var TEMPLATES = [
|
var TEMPLATES = [
|
||||||
"",
|
"",
|
||||||
{% for t in payment_method.templates %}
|
{% for t in payment_method.templates %}
|
||||||
{% if t %}
|
{% if t %}
|
||||||
`{% bootstrap_form t %}`,
|
`{% bootstrap_form t %}`,
|
||||||
{% else %}
|
{% else %}
|
||||||
"",
|
"",
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
];
|
];
|
||||||
function update_payment_method_form(){
|
function update_payment_method_form(){
|
||||||
var method = document.getElementById('paymentMethodSelect').value;
|
var method = document.getElementById('paymentMethodSelect').value;
|
||||||
if(method==""){
|
if(method==""){
|
||||||
method=0;
|
method=0;
|
||||||
}
|
|
||||||
else{
|
|
||||||
method = Number(method);
|
|
||||||
method += 1;
|
|
||||||
}
|
|
||||||
console.log(method);
|
|
||||||
var html = TEMPLATES[method];
|
|
||||||
|
|
||||||
document.getElementById('paymentMethod').innerHTML = html;
|
|
||||||
}
|
}
|
||||||
document.getElementById("paymentMethodSelect").addEventListener("change", update_payment_method_form);
|
else{
|
||||||
|
method = Number(method);
|
||||||
|
method += 1;
|
||||||
|
}
|
||||||
|
console.log(method);
|
||||||
|
var html = TEMPLATES[method];
|
||||||
|
|
||||||
|
document.getElementById('paymentMethod').innerHTML = html;
|
||||||
|
}
|
||||||
|
document.getElementById("paymentMethodSelect").addEventListener("change", update_payment_method_form);
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
|
|
||||||
|
|
||||||
\begin{document}
|
\begin{document}
|
||||||
|
|
||||||
%----------------------------------------------------------------------------------------
|
%----------------------------------------------------------------------------------------
|
||||||
% HEADING SECTION
|
% HEADING SECTION
|
||||||
%----------------------------------------------------------------------------------------
|
%----------------------------------------------------------------------------------------
|
||||||
|
@ -70,13 +70,17 @@
|
||||||
{\bf Siret :} {{siret|safe}}
|
{\bf Siret :} {{siret|safe}}
|
||||||
|
|
||||||
\vspace{2cm}
|
\vspace{2cm}
|
||||||
|
|
||||||
\begin{tabular*}{\textwidth}{@{\extracolsep{\fill}} l r}
|
\begin{tabular*}{\textwidth}{@{\extracolsep{\fill}} l r}
|
||||||
{\bf Pour :} {{recipient_name|safe}} & {\bf Date :} {{DATE}} \\
|
{\bf Pour :} {{recipient_name|safe}} & {\bf Date :} {{DATE}} \\
|
||||||
{\bf Adresse :} {% if address is None %}Aucune adresse renseignée{% else %}{{address}}{% endif %} & \\
|
{\bf Adresse :} {% if address is None %}Aucune adresse renseignée{% else %}{{address}}{% endif %} & \\
|
||||||
{% if fid is not None %}
|
{% if fid is not None %}
|
||||||
|
{% if is_estimate %}
|
||||||
|
{\bf Devis n\textsuperscript{o} :} {{ fid }} & \\
|
||||||
|
{% else %}
|
||||||
{\bf Facture n\textsuperscript{o} :} {{ fid }} & \\
|
{\bf Facture n\textsuperscript{o} :} {{ fid }} & \\
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
\end{tabular*}
|
\end{tabular*}
|
||||||
\\
|
\\
|
||||||
|
|
||||||
|
@ -84,39 +88,57 @@
|
||||||
%----------------------------------------------------------------------------------------
|
%----------------------------------------------------------------------------------------
|
||||||
% TABLE OF EXPENSES
|
% TABLE OF EXPENSES
|
||||||
%----------------------------------------------------------------------------------------
|
%----------------------------------------------------------------------------------------
|
||||||
|
|
||||||
\begin{tabularx}{\textwidth}{|X|r|r|r|}
|
\begin{tabularx}{\textwidth}{|X|r|r|r|}
|
||||||
|
|
||||||
\hline
|
\hline
|
||||||
\textbf{Désignation} & \textbf{Prix Unit.} \euro & \textbf{Quantité} & \textbf{Prix total} \euro\\
|
\textbf{Désignation} & \textbf{Prix Unit.} \euro & \textbf{Quantité} & \textbf{Prix total} \euro\\
|
||||||
\doublehline
|
\doublehline
|
||||||
|
|
||||||
{% for a in article %}
|
{% for a in article %}
|
||||||
{{a.name}} & {{a.price}} \euro & {{a.quantity}} & {{a.total_price}} \euro\\
|
{{a.name}} & {{a.price}} \euro & {{a.quantity}} & {{a.total_price}} \euro\\
|
||||||
\hline
|
\hline
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
\end{tabularx}
|
\end{tabularx}
|
||||||
|
|
||||||
\vspace{1cm}
|
\vspace{1cm}
|
||||||
|
|
||||||
\hfill
|
\hfill
|
||||||
\begin{tabular}{|l|r|}
|
\begin{tabular}{|l|r|}
|
||||||
\hline
|
\hline
|
||||||
\textbf{Total} & {{total|floatformat:2}} \euro \\
|
\textbf{Total} & {{total|floatformat:2}} \euro \\
|
||||||
|
{% if not is_estimate %}
|
||||||
\textbf{Votre règlement} & {% if paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro \\
|
\textbf{Votre règlement} & {% if paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro \\
|
||||||
\doublehline
|
\doublehline
|
||||||
\textbf{À PAYER} & {% if not paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro\\
|
\textbf{À PAYER} & {% if not paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro\\
|
||||||
|
{% endif %}
|
||||||
\hline
|
\hline
|
||||||
\end{tabular}
|
\end{tabular}
|
||||||
|
|
||||||
|
\vspace{1cm}
|
||||||
|
\begin{tabularx}{\textwidth}{r X}
|
||||||
|
\hline
|
||||||
|
\textbf{Moyen de paiement} & {{payment_method|default:"Non spécifié"}} \\
|
||||||
|
\hline
|
||||||
|
{% if remark %}
|
||||||
|
\textbf{Remarque} & {{remark|safe}} \\
|
||||||
|
\hline
|
||||||
|
{% endif %}
|
||||||
|
{% if end_validity %}
|
||||||
|
\textbf{Validité} & Jusqu'au {{end_validity}} \\
|
||||||
|
\hline
|
||||||
|
{% endif %}
|
||||||
|
\end{tabularx}
|
||||||
|
|
||||||
|
|
||||||
\vfill
|
\vfill
|
||||||
|
|
||||||
|
|
||||||
%----------------------------------------------------------------------------------------
|
%----------------------------------------------------------------------------------------
|
||||||
% FOOTNOTE
|
% FOOTNOTE
|
||||||
%----------------------------------------------------------------------------------------
|
%----------------------------------------------------------------------------------------
|
||||||
|
|
||||||
\hrule
|
\hrule
|
||||||
\smallskip
|
\smallskip
|
||||||
\footnotesize{TVA non applicable, art. 293 B du CGI}
|
\footnotesize{TVA non applicable, art. 293 B du CGI}
|
||||||
|
|
36
cotisations/templates/cotisations/index_cost_estimate.html
Normal file
36
cotisations/templates/cotisations/index_cost_estimate.html
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
{% extends "cotisations/sidebar.html" %}
|
||||||
|
{% comment %}
|
||||||
|
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
||||||
|
se veut agnostique au réseau considéré, de manière à être installable en
|
||||||
|
quelques clics.
|
||||||
|
|
||||||
|
Copyright © 2017 Gabriel Détraz
|
||||||
|
Copyright © 2017 Goulven Kermarec
|
||||||
|
Copyright © 2017 Augustin Lemesle
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
{% endcomment %}
|
||||||
|
{% load acl %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{% trans "Cost estimates" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>{% trans "Cost estimates list" %}</h2>
|
||||||
|
{% can_create CostEstimate %}
|
||||||
|
{% include "buttons/add.html" with href='cotisations:new-cost-estimate'%}
|
||||||
|
{% acl_end %}
|
||||||
|
{% include 'cotisations/aff_cost_estimate.html' %}
|
||||||
|
{% endblock %}
|
|
@ -45,6 +45,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
<i class="fa fa-list-ul"></i> {% trans "Custom invoices" %}
|
<i class="fa fa-list-ul"></i> {% trans "Custom invoices" %}
|
||||||
</a>
|
</a>
|
||||||
{% acl_end %}
|
{% acl_end %}
|
||||||
|
{% can_view_all CostEstimate %}
|
||||||
|
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-cost-estimate" %}">
|
||||||
|
<i class="fa fa-list-ul"></i> {% trans "Cost estimate" %}
|
||||||
|
</a>
|
||||||
|
{% acl_end %}
|
||||||
{% can_view_all Article %}
|
{% can_view_all Article %}
|
||||||
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}">
|
<a class="list-group-item list-group-item-info" href="{% url "cotisations:index-article" %}">
|
||||||
<i class="fa fa-list-ul"></i> {% trans "Available articles" %}
|
<i class="fa fa-list-ul"></i> {% trans "Available articles" %}
|
||||||
|
|
|
@ -36,6 +36,7 @@ from django.template import Context
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
TEMP_PREFIX = getattr(settings, 'TEX_TEMP_PREFIX', 'render_tex-')
|
TEMP_PREFIX = getattr(settings, 'TEX_TEMP_PREFIX', 'render_tex-')
|
||||||
|
@ -48,8 +49,9 @@ def render_invoice(_request, ctx={}):
|
||||||
Render an invoice using some available information such as the current
|
Render an invoice using some available information such as the current
|
||||||
date, the user, the articles, the prices, ...
|
date, the user, the articles, the prices, ...
|
||||||
"""
|
"""
|
||||||
|
is_estimate = ctx.get('is_estimate', False)
|
||||||
filename = '_'.join([
|
filename = '_'.join([
|
||||||
'invoice',
|
'cost_estimate' if is_estimate else 'invoice',
|
||||||
slugify(ctx.get('asso_name', "")),
|
slugify(ctx.get('asso_name', "")),
|
||||||
slugify(ctx.get('recipient_name', "")),
|
slugify(ctx.get('recipient_name', "")),
|
||||||
str(ctx.get('DATE', datetime.now()).year),
|
str(ctx.get('DATE', datetime.now()).year),
|
||||||
|
@ -93,6 +95,20 @@ def create_pdf(template, ctx={}):
|
||||||
return pdf
|
return pdf
|
||||||
|
|
||||||
|
|
||||||
|
def escape_chars(string):
|
||||||
|
"""Escape the '%' and the '€' signs to avoid messing with LaTeX"""
|
||||||
|
if not isinstance(string, str):
|
||||||
|
return string
|
||||||
|
mapping = (
|
||||||
|
('€', r'\euro'),
|
||||||
|
('%', r'\%'),
|
||||||
|
)
|
||||||
|
r = str(string)
|
||||||
|
for k, v in mapping:
|
||||||
|
r = r.replace(k, v)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
def render_tex(_request, template, ctx={}):
|
def render_tex(_request, template, ctx={}):
|
||||||
"""Creates a PDF from a LaTex templates using pdflatex.
|
"""Creates a PDF from a LaTex templates using pdflatex.
|
||||||
|
|
||||||
|
|
|
@ -51,11 +51,41 @@ urlpatterns = [
|
||||||
views.facture_pdf,
|
views.facture_pdf,
|
||||||
name='facture-pdf'
|
name='facture-pdf'
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r'^new_cost_estimate/$',
|
||||||
|
views.new_cost_estimate,
|
||||||
|
name='new-cost-estimate'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^index_cost_estimate/$',
|
||||||
|
views.index_cost_estimate,
|
||||||
|
name='index-cost-estimate'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^cost_estimate_pdf/(?P<costestimateid>[0-9]+)$',
|
||||||
|
views.cost_estimate_pdf,
|
||||||
|
name='cost-estimate-pdf',
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r'^index_custom_invoice/$',
|
r'^index_custom_invoice/$',
|
||||||
views.index_custom_invoice,
|
views.index_custom_invoice,
|
||||||
name='index-custom-invoice'
|
name='index-custom-invoice'
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r'^edit_cost_estimate/(?P<costestimateid>[0-9]+)$',
|
||||||
|
views.edit_cost_estimate,
|
||||||
|
name='edit-cost-estimate'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^cost_estimate_to_invoice/(?P<costestimateid>[0-9]+)$',
|
||||||
|
views.cost_estimate_to_invoice,
|
||||||
|
name='cost-estimate-to-invoice'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^del_cost_estimate/(?P<costestimateid>[0-9]+)$',
|
||||||
|
views.del_cost_estimate,
|
||||||
|
name='del-cost-estimate'
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r'^new_custom_invoice/$',
|
r'^new_custom_invoice/$',
|
||||||
views.new_custom_invoice,
|
views.new_custom_invoice,
|
||||||
|
|
|
@ -68,7 +68,8 @@ from .models import (
|
||||||
Paiement,
|
Paiement,
|
||||||
Banque,
|
Banque,
|
||||||
CustomInvoice,
|
CustomInvoice,
|
||||||
BaseInvoice
|
BaseInvoice,
|
||||||
|
CostEstimate
|
||||||
)
|
)
|
||||||
from .forms import (
|
from .forms import (
|
||||||
FactureForm,
|
FactureForm,
|
||||||
|
@ -80,9 +81,11 @@ from .forms import (
|
||||||
DelBanqueForm,
|
DelBanqueForm,
|
||||||
SelectArticleForm,
|
SelectArticleForm,
|
||||||
RechargeForm,
|
RechargeForm,
|
||||||
CustomInvoiceForm
|
CustomInvoiceForm,
|
||||||
|
DiscountForm,
|
||||||
|
CostEstimateForm,
|
||||||
)
|
)
|
||||||
from .tex import render_invoice
|
from .tex import render_invoice, escape_chars
|
||||||
from .payment_methods.forms import payment_method_factory
|
from .payment_methods.forms import payment_method_factory
|
||||||
from .utils import find_payment_method
|
from .utils import find_payment_method
|
||||||
|
|
||||||
|
@ -178,7 +181,58 @@ def new_facture(request, user, userid):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# TODO : change facture to invoice
|
@login_required
|
||||||
|
@can_create(CostEstimate)
|
||||||
|
def new_cost_estimate(request):
|
||||||
|
"""
|
||||||
|
View used to generate a custom invoice. It's mainly used to
|
||||||
|
get invoices that are not taken into account, for the administrative
|
||||||
|
point of view.
|
||||||
|
"""
|
||||||
|
# The template needs the list of articles (for the JS part)
|
||||||
|
articles = Article.objects.filter(
|
||||||
|
Q(type_user='All') | Q(type_user=request.user.class_name)
|
||||||
|
)
|
||||||
|
# Building the invocie form and the article formset
|
||||||
|
cost_estimate_form = CostEstimateForm(request.POST or None)
|
||||||
|
|
||||||
|
articles_formset = formset_factory(SelectArticleForm)(
|
||||||
|
request.POST or None,
|
||||||
|
form_kwargs={'user': request.user}
|
||||||
|
)
|
||||||
|
discount_form = DiscountForm(request.POST or None)
|
||||||
|
|
||||||
|
if cost_estimate_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid():
|
||||||
|
cost_estimate_instance = cost_estimate_form.save()
|
||||||
|
for art_item in articles_formset:
|
||||||
|
if art_item.cleaned_data:
|
||||||
|
article = art_item.cleaned_data['article']
|
||||||
|
quantity = art_item.cleaned_data['quantity']
|
||||||
|
Vente.objects.create(
|
||||||
|
facture=cost_estimate_instance,
|
||||||
|
name=article.name,
|
||||||
|
prix=article.prix,
|
||||||
|
type_cotisation=article.type_cotisation,
|
||||||
|
duration=article.duration,
|
||||||
|
number=quantity
|
||||||
|
)
|
||||||
|
discount_form.apply_to_invoice(cost_estimate_instance)
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
_("The cost estimate was created.")
|
||||||
|
)
|
||||||
|
return redirect(reverse('cotisations:index-cost-estimate'))
|
||||||
|
|
||||||
|
return form({
|
||||||
|
'factureform': cost_estimate_form,
|
||||||
|
'action_name': _("Confirm"),
|
||||||
|
'articlesformset': articles_formset,
|
||||||
|
'articlelist': articles,
|
||||||
|
'discount_form': discount_form,
|
||||||
|
'title': _("Cost estimate"),
|
||||||
|
}, 'cotisations/facture.html', request)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@can_create(CustomInvoice)
|
@can_create(CustomInvoice)
|
||||||
def new_custom_invoice(request):
|
def new_custom_invoice(request):
|
||||||
|
@ -198,8 +252,9 @@ def new_custom_invoice(request):
|
||||||
request.POST or None,
|
request.POST or None,
|
||||||
form_kwargs={'user': request.user}
|
form_kwargs={'user': request.user}
|
||||||
)
|
)
|
||||||
|
discount_form = DiscountForm(request.POST or None)
|
||||||
|
|
||||||
if invoice_form.is_valid() and articles_formset.is_valid():
|
if invoice_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid():
|
||||||
new_invoice_instance = invoice_form.save()
|
new_invoice_instance = invoice_form.save()
|
||||||
for art_item in articles_formset:
|
for art_item in articles_formset:
|
||||||
if art_item.cleaned_data:
|
if art_item.cleaned_data:
|
||||||
|
@ -213,6 +268,7 @@ def new_custom_invoice(request):
|
||||||
duration=article.duration,
|
duration=article.duration,
|
||||||
number=quantity
|
number=quantity
|
||||||
)
|
)
|
||||||
|
discount_form.apply_to_invoice(new_invoice_instance)
|
||||||
messages.success(
|
messages.success(
|
||||||
request,
|
request,
|
||||||
_("The custom invoice was created.")
|
_("The custom invoice was created.")
|
||||||
|
@ -223,7 +279,8 @@ def new_custom_invoice(request):
|
||||||
'factureform': invoice_form,
|
'factureform': invoice_form,
|
||||||
'action_name': _("Confirm"),
|
'action_name': _("Confirm"),
|
||||||
'articlesformset': articles_formset,
|
'articlesformset': articles_formset,
|
||||||
'articlelist': articles
|
'articlelist': articles,
|
||||||
|
'discount_form': discount_form
|
||||||
}, 'cotisations/facture.html', request)
|
}, 'cotisations/facture.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -266,7 +323,8 @@ def facture_pdf(request, facture, **_kwargs):
|
||||||
'siret': AssoOption.get_cached_value('siret'),
|
'siret': AssoOption.get_cached_value('siret'),
|
||||||
'email': AssoOption.get_cached_value('contact'),
|
'email': AssoOption.get_cached_value('contact'),
|
||||||
'phone': AssoOption.get_cached_value('telephone'),
|
'phone': AssoOption.get_cached_value('telephone'),
|
||||||
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
|
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
|
||||||
|
'payment_method': facture.paiement.moyen,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -331,6 +389,55 @@ def del_facture(request, facture, **_kwargs):
|
||||||
}, 'cotisations/delete.html', request)
|
}, 'cotisations/delete.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_edit(CostEstimate)
|
||||||
|
def edit_cost_estimate(request, invoice, **kwargs):
|
||||||
|
# Building the invocie form and the article formset
|
||||||
|
invoice_form = CostEstimateForm(
|
||||||
|
request.POST or None,
|
||||||
|
instance=invoice
|
||||||
|
)
|
||||||
|
purchases_objects = Vente.objects.filter(facture=invoice)
|
||||||
|
purchase_form_set = modelformset_factory(
|
||||||
|
Vente,
|
||||||
|
fields=('name', 'number'),
|
||||||
|
extra=0,
|
||||||
|
max_num=len(purchases_objects)
|
||||||
|
)
|
||||||
|
purchase_form = purchase_form_set(
|
||||||
|
request.POST or None,
|
||||||
|
queryset=purchases_objects
|
||||||
|
)
|
||||||
|
if invoice_form.is_valid() and purchase_form.is_valid():
|
||||||
|
if invoice_form.changed_data:
|
||||||
|
invoice_form.save()
|
||||||
|
purchase_form.save()
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
_("The cost estimate was edited.")
|
||||||
|
)
|
||||||
|
return redirect(reverse('cotisations:index-cost-estimate'))
|
||||||
|
|
||||||
|
return form({
|
||||||
|
'factureform': invoice_form,
|
||||||
|
'venteform': purchase_form,
|
||||||
|
'title': "Edit the cost estimate"
|
||||||
|
}, 'cotisations/edit_facture.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_edit(CostEstimate)
|
||||||
|
@can_create(CustomInvoice)
|
||||||
|
def cost_estimate_to_invoice(request, cost_estimate, **_kwargs):
|
||||||
|
"""Create a custom invoice from a cos estimate"""
|
||||||
|
cost_estimate.create_invoice()
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
_("An invoice was successfully created from your cost estimate.")
|
||||||
|
)
|
||||||
|
return redirect(reverse('cotisations:index-custom-invoice'))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@can_edit(CustomInvoice)
|
@can_edit(CustomInvoice)
|
||||||
def edit_custom_invoice(request, invoice, **kwargs):
|
def edit_custom_invoice(request, invoice, **kwargs):
|
||||||
|
@ -367,10 +474,10 @@ def edit_custom_invoice(request, invoice, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@can_view(CustomInvoice)
|
@can_view(CostEstimate)
|
||||||
def custom_invoice_pdf(request, invoice, **_kwargs):
|
def cost_estimate_pdf(request, invoice, **_kwargs):
|
||||||
"""
|
"""
|
||||||
View used to generate a PDF file from an existing invoice in database
|
View used to generate a PDF file from an existing cost estimate in database
|
||||||
Creates a line for each Purchase (thus article sold) and generate the
|
Creates a line for each Purchase (thus article sold) and generate the
|
||||||
invoice with the total price, the payment method, the address and the
|
invoice with the total price, the payment method, the address and the
|
||||||
legal information for the user.
|
legal information for the user.
|
||||||
|
@ -382,7 +489,7 @@ def custom_invoice_pdf(request, invoice, **_kwargs):
|
||||||
purchases_info = []
|
purchases_info = []
|
||||||
for purchase in purchases_objects:
|
for purchase in purchases_objects:
|
||||||
purchases_info.append({
|
purchases_info.append({
|
||||||
'name': purchase.name,
|
'name': escape_chars(purchase.name),
|
||||||
'price': purchase.prix,
|
'price': purchase.prix,
|
||||||
'quantity': purchase.number,
|
'quantity': purchase.number,
|
||||||
'total_price': purchase.prix_total
|
'total_price': purchase.prix_total
|
||||||
|
@ -401,11 +508,74 @@ def custom_invoice_pdf(request, invoice, **_kwargs):
|
||||||
'siret': AssoOption.get_cached_value('siret'),
|
'siret': AssoOption.get_cached_value('siret'),
|
||||||
'email': AssoOption.get_cached_value('contact'),
|
'email': AssoOption.get_cached_value('contact'),
|
||||||
'phone': AssoOption.get_cached_value('telephone'),
|
'phone': AssoOption.get_cached_value('telephone'),
|
||||||
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH)
|
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
|
||||||
|
'payment_method': invoice.payment,
|
||||||
|
'remark': invoice.remark,
|
||||||
|
'end_validity': invoice.date + invoice.validity,
|
||||||
|
'is_estimate': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_delete(CostEstimate)
|
||||||
|
def del_cost_estimate(request, estimate, **_kwargs):
|
||||||
|
"""
|
||||||
|
View used to delete an existing invocie.
|
||||||
|
"""
|
||||||
|
if request.method == "POST":
|
||||||
|
estimate.delete()
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
_("The cost estimate was deleted.")
|
||||||
|
)
|
||||||
|
return redirect(reverse('cotisations:index-cost-estimate'))
|
||||||
|
return form({
|
||||||
|
'objet': estimate,
|
||||||
|
'objet_name': _("Cost Estimate")
|
||||||
|
}, 'cotisations/delete.html', request)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_view(CustomInvoice)
|
||||||
|
def custom_invoice_pdf(request, invoice, **_kwargs):
|
||||||
|
"""
|
||||||
|
View used to generate a PDF file from an existing invoice in database
|
||||||
|
Creates a line for each Purchase (thus article sold) and generate the
|
||||||
|
invoice with the total price, the payment method, the address and the
|
||||||
|
legal information for the user.
|
||||||
|
"""
|
||||||
|
# TODO : change vente to purchase
|
||||||
|
purchases_objects = Vente.objects.all().filter(facture=invoice)
|
||||||
|
# Get the article list and build an list out of it
|
||||||
|
# contiaining (article_name, article_price, quantity, total_price)
|
||||||
|
purchases_info = []
|
||||||
|
for purchase in purchases_objects:
|
||||||
|
purchases_info.append({
|
||||||
|
'name': escape_chars(purchase.name),
|
||||||
|
'price': purchase.prix,
|
||||||
|
'quantity': purchase.number,
|
||||||
|
'total_price': purchase.prix_total
|
||||||
|
})
|
||||||
|
return render_invoice(request, {
|
||||||
|
'paid': invoice.paid,
|
||||||
|
'fid': invoice.id,
|
||||||
|
'DATE': invoice.date,
|
||||||
|
'recipient_name': invoice.recipient,
|
||||||
|
'address': invoice.address,
|
||||||
|
'article': purchases_info,
|
||||||
|
'total': invoice.prix_total(),
|
||||||
|
'asso_name': AssoOption.get_cached_value('name'),
|
||||||
|
'line1': AssoOption.get_cached_value('adresse1'),
|
||||||
|
'line2': AssoOption.get_cached_value('adresse2'),
|
||||||
|
'siret': AssoOption.get_cached_value('siret'),
|
||||||
|
'email': AssoOption.get_cached_value('contact'),
|
||||||
|
'phone': AssoOption.get_cached_value('telephone'),
|
||||||
|
'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH),
|
||||||
|
'payment_method': invoice.payment,
|
||||||
|
'remark': invoice.remark,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
# TODO : change facture to invoice
|
|
||||||
@login_required
|
@login_required
|
||||||
@can_delete(CustomInvoice)
|
@can_delete(CustomInvoice)
|
||||||
def del_custom_invoice(request, invoice, **_kwargs):
|
def del_custom_invoice(request, invoice, **_kwargs):
|
||||||
|
@ -756,12 +926,35 @@ def index_banque(request):
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@can_view_all(CustomInvoice)
|
||||||
|
def index_cost_estimate(request):
|
||||||
|
"""View used to display every custom invoice."""
|
||||||
|
pagination_number = GeneralOption.get_cached_value('pagination_number')
|
||||||
|
cost_estimate_list = CostEstimate.objects.prefetch_related('vente_set')
|
||||||
|
cost_estimate_list = SortTable.sort(
|
||||||
|
cost_estimate_list,
|
||||||
|
request.GET.get('col'),
|
||||||
|
request.GET.get('order'),
|
||||||
|
SortTable.COTISATIONS_CUSTOM
|
||||||
|
)
|
||||||
|
cost_estimate_list = re2o_paginator(
|
||||||
|
request,
|
||||||
|
cost_estimate_list,
|
||||||
|
pagination_number,
|
||||||
|
)
|
||||||
|
return render(request, 'cotisations/index_cost_estimate.html', {
|
||||||
|
'cost_estimate_list': cost_estimate_list
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@can_view_all(CustomInvoice)
|
@can_view_all(CustomInvoice)
|
||||||
def index_custom_invoice(request):
|
def index_custom_invoice(request):
|
||||||
"""View used to display every custom invoice."""
|
"""View used to display every custom invoice."""
|
||||||
pagination_number = GeneralOption.get_cached_value('pagination_number')
|
pagination_number = GeneralOption.get_cached_value('pagination_number')
|
||||||
custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set')
|
cost_estimate_ids = [i for i, in CostEstimate.objects.values_list('id')]
|
||||||
|
custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set').exclude(id__in=cost_estimate_ids)
|
||||||
custom_invoice_list = SortTable.sort(
|
custom_invoice_list = SortTable.sort(
|
||||||
custom_invoice_list,
|
custom_invoice_list,
|
||||||
request.GET.get('col'),
|
request.GET.get('col'),
|
||||||
|
|
Loading…
Reference in a new issue