invoicing: add basic appearance settings (#80893)
gitea/lingo/pipeline/head This commit looks good
Details
gitea/lingo/pipeline/head This commit looks good
Details
This commit is contained in:
parent
08aafb37a9
commit
2e024347f2
|
@ -0,0 +1,34 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
import lingo.utils.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('invoicing', '0060_journal_line_booking'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AppearanceSettings',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('logo', models.ImageField(blank=True, null=True, upload_to='logo', verbose_name='Logo')),
|
||||
('address', lingo.utils.fields.RichTextField(blank=True, null=True, verbose_name='Address')),
|
||||
(
|
||||
'extra_info',
|
||||
lingo.utils.fields.RichTextField(blank=True, null=True, verbose_name='Extra info'),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='regie',
|
||||
name='invoice_custom_text',
|
||||
field=lingo.utils.fields.RichTextField(
|
||||
blank=True, null=True, verbose_name='Custom text in invoice'
|
||||
),
|
||||
),
|
||||
]
|
|
@ -14,6 +14,7 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import base64
|
||||
import copy
|
||||
import datetime
|
||||
import decimal
|
||||
|
@ -28,6 +29,7 @@ from django.core.serializers.json import DjangoJSONEncoder
|
|||
from django.db import connection, models, transaction
|
||||
from django.template import RequestContext, Template, TemplateSyntaxError, VariableDoesNotExist
|
||||
from django.template.loader import get_template
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.text import slugify
|
||||
from django.utils.timezone import now
|
||||
|
@ -35,6 +37,7 @@ from django.utils.translation import gettext_lazy as _
|
|||
|
||||
from lingo.agendas.chrono import ChronoError, lock_events_check
|
||||
from lingo.agendas.models import Agenda
|
||||
from lingo.utils.fields import RichTextField
|
||||
from lingo.utils.misc import LingoImportError, clean_import_data, generate_slug
|
||||
from lingo.utils.wcs import get_wcs_json, get_wcs_matching_card_model, get_wcs_services
|
||||
|
||||
|
@ -283,6 +286,11 @@ class Regie(models.Model):
|
|||
default='R{regie_id:02d}-{yy}-{mm}-{number:07d}',
|
||||
max_length=100,
|
||||
)
|
||||
invoice_custom_text = RichTextField(
|
||||
_('Custom text in invoice'),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['label']
|
||||
|
@ -656,6 +664,7 @@ class AbstractInvoice(models.Model):
|
|||
'regie': self.regie,
|
||||
'invoice': self,
|
||||
'lines': self.get_grouped_and_ordered_lines(),
|
||||
'appearance_settings': AppearanceSettings.singleton(),
|
||||
}
|
||||
return template.render(context)
|
||||
|
||||
|
@ -1110,3 +1119,29 @@ class InvoicePayment(models.Model):
|
|||
max_digits=9, decimal_places=2, validators=[validators.MinValueValidator(decimal.Decimal('0.01'))]
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class AppearanceSettings(models.Model):
|
||||
logo = models.ImageField(
|
||||
verbose_name=_('Logo'),
|
||||
upload_to='logo',
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
address = RichTextField(
|
||||
verbose_name=_('Address'),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
extra_info = RichTextField(
|
||||
verbose_name=_('Extra info'),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def singleton(cls):
|
||||
return cls.objects.first() or cls()
|
||||
|
||||
def logo_base64_encoded(self):
|
||||
return force_str(base64.encodebytes(self.logo.read()))
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
{% extends "lingo/invoicing/manager_regie_list.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block extrascripts %}
|
||||
{{ block.super }}
|
||||
{% include "lingo/includes/ckeditor-js.html" only %}
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'lingo-manager-invoicing-appearance-settings' %}">{% trans "Appearance Settings" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans "Appearance" %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<div class="buttons">
|
||||
<button class="submit-button">{% trans "Save" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -6,47 +6,75 @@
|
|||
<meta name="author" content="Entr'ouvert">
|
||||
<style>
|
||||
html {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: #14213d;
|
||||
font-family: Source Sans Pro;
|
||||
font-size: 8pt;
|
||||
font-family: "DejaVu Sans", sans-serif;
|
||||
font-size: 9pt;
|
||||
line-height: 1.2;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media screen {
|
||||
body {
|
||||
margin: auto;
|
||||
width: 21cm;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
color: #df5a13;
|
||||
font-family: Pacifico;
|
||||
margin: 0.4em 0;
|
||||
font-size: 12pt;
|
||||
}
|
||||
h2 {
|
||||
margin: 2em 0;
|
||||
margin: 0.4em 0;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
aside {
|
||||
display: flex;
|
||||
margin: 2em 0 4em;
|
||||
}
|
||||
aside address {
|
||||
font-style: normal;
|
||||
white-space: pre-line;
|
||||
}
|
||||
aside address#from {
|
||||
color: #a9a;
|
||||
flex: 1;
|
||||
}
|
||||
aside address#to {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
dl {
|
||||
header {
|
||||
/* top of page, addresses, can be folded */
|
||||
position: absolute;
|
||||
right: 0;
|
||||
text-align: right;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
#logo {
|
||||
max-height: 4cm;
|
||||
max-width: 4cm;
|
||||
}
|
||||
main {
|
||||
/* page content, after the fold */
|
||||
padding-top: 9cm;
|
||||
}
|
||||
header aside {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 30%;
|
||||
}
|
||||
header > div {
|
||||
margin-left: 30%;
|
||||
}
|
||||
address {
|
||||
font-style: normal;
|
||||
}
|
||||
address#to {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
padding: 0.5cm;
|
||||
top: 3cm; /* in addition to page margin (2cm) */
|
||||
left: 8cm;
|
||||
width: 10cm;
|
||||
height: 5cm;
|
||||
background: #fafafa;
|
||||
}
|
||||
#extra-info {
|
||||
}
|
||||
|
||||
dl#informations {
|
||||
margin-left: 30%; /* same as header/aside width */
|
||||
margin-bottom: 1cm;
|
||||
}
|
||||
dt, dd {
|
||||
display: inline;
|
||||
|
@ -59,10 +87,6 @@
|
|||
content: '';
|
||||
display: block;
|
||||
}
|
||||
dt::after {
|
||||
content: ':';
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
|
@ -79,32 +103,36 @@
|
|||
text-transform: uppercase;
|
||||
}
|
||||
td.total-amount {
|
||||
color: #df5a13;
|
||||
font-weight: bold;
|
||||
}
|
||||
tr.grand-total {
|
||||
background: #eee;
|
||||
border-top: 0.5cm solid white;
|
||||
}
|
||||
tr.grand-total td {
|
||||
padding-top: 0.5cm;
|
||||
padding-bottom: 0.5cm;
|
||||
}
|
||||
td.grand-total-intro {
|
||||
text-align: right;
|
||||
}
|
||||
td.details {
|
||||
vertical-align: top;
|
||||
}
|
||||
th.amount, td.amount, th.quantity, td.quantity {
|
||||
text-align: right;
|
||||
}
|
||||
table#total {
|
||||
background: #f6f6f6;
|
||||
border-color: #f6f6f6;
|
||||
border-style: solid;
|
||||
border-width: 2cm 3cm;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
font-size: 10pt;
|
||||
margin: 0 -3cm;
|
||||
position: absolute;
|
||||
width: 12cm;
|
||||
padding-right: 0.2cm;
|
||||
text-align: right;
|
||||
}
|
||||
div.break {
|
||||
break-before: page;
|
||||
}
|
||||
#regie-custom-text {
|
||||
margin: auto;
|
||||
margin-top: 2cm;
|
||||
max-width: 55em;
|
||||
}
|
||||
@page {
|
||||
margin: 1cm;
|
||||
@bottom-right{
|
||||
content: "{% trans "Page" %} " counter(page) "/" counter(pages);
|
||||
}
|
||||
|
@ -112,92 +140,109 @@
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ invoice.label }}</h1>
|
||||
<aside>
|
||||
<address id="from">
|
||||
{{ regie.label }}
|
||||
</address>
|
||||
<address id="to">
|
||||
{{ invoice.payer_first_name}} {{ invoice.payer_last_name}}
|
||||
</address>
|
||||
</aside>
|
||||
<dl id="informations">
|
||||
<dt>{% trans "Invoice number" %}</dt>
|
||||
<dd>{{ invoice.formatted_number }}</dd>
|
||||
<dt>{% trans "Date" %}</dt>
|
||||
<dd>{{ invoice.created_at|date }}</dd>
|
||||
</dl>
|
||||
{% for line in lines %}
|
||||
{% ifchanged line.user_external_id %}
|
||||
{% if not forloop.first %}
|
||||
</tbody>
|
||||
</table>
|
||||
<header>
|
||||
<aside>
|
||||
{% if appearance_settings.logo %}
|
||||
<img id="logo" src="data:{{appearance_settings.logo.content_type}};base64,{{ appearance_settings.logo_base64_encoded }}">
|
||||
{% endif %}
|
||||
<h2>{{ line.user_name }}</h2>
|
||||
<table id="lines">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="description">{% trans "Description" %}</th>
|
||||
<th class="details">{% trans "Details" %}</th>
|
||||
<th class="amount unit-amount">{% trans "UA" context 'unit amount' %}</th>
|
||||
<th class="quantity">{% trans "QTY" context 'quantity' %}</th>
|
||||
<th class="amount total-amount">{% trans "TA" context 'total amount' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% endifchanged %}
|
||||
<tr {% if not line.details %}class="new-event"{% else %}{% ifchanged line.label %}class="new-event"{% endifchanged %}{% endif %}>
|
||||
<td class="description">
|
||||
{% if line.details %}
|
||||
{% ifchanged line.user_external_id line.label %}
|
||||
{{ line.label }}
|
||||
{% if appearance_settings.address %}
|
||||
<address id="from">
|
||||
{{ appearance_settings.address|safe }}
|
||||
</address>
|
||||
{% endif %}
|
||||
{% if appearance_settings.extra_info %}
|
||||
<div id="extra-info">
|
||||
{{ appearance_settings.extra_info|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</aside>
|
||||
<div>
|
||||
<h1 id="invoice-label">{{ invoice.label }}</h1>
|
||||
<h2 id="regie-label">{{ regie.label }}</h2>
|
||||
<address id="to">
|
||||
{{ invoice.payer_first_name}} {{ invoice.payer_last_name}}
|
||||
</address>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<dl id="informations">
|
||||
<dt>{% trans "Invoice number:" %}</dt>
|
||||
<dd>{{ invoice.formatted_number }}</dd>
|
||||
<dt>{% trans "Date:" %}</dt>
|
||||
<dd>{{ invoice.created_at|date }}</dd>
|
||||
</dl>
|
||||
{% for line in lines %}
|
||||
{% ifchanged line.user_external_id %}
|
||||
{% if not forloop.first %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
<h2 class="user-name">{{ line.user_name }}</h2>
|
||||
<table id="lines">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="description">{% trans "Description" %}</th>
|
||||
<th class="details">{% trans "Details" %}</th>
|
||||
<th class="amount unit-amount">{% trans "UA" context 'unit amount' %}</th>
|
||||
<th class="quantity">{% trans "QTY" context 'quantity' %}</th>
|
||||
<th class="amount total-amount">{% trans "TA" context 'total amount' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% endifchanged %}
|
||||
<tr {% if not line.details %}class="new-event"{% else %}{% ifchanged line.label %}class="new-event"{% endifchanged %}{% endif %}>
|
||||
<td class="description">
|
||||
{% if line.details %}
|
||||
{% ifchanged line.user_external_id line.label %}
|
||||
{{ line.label }}
|
||||
<br />
|
||||
{% if line.agenda %}
|
||||
<i>{{ line.agenda.label }}</i>
|
||||
<br />
|
||||
{% endif %}
|
||||
{% endifchanged %}
|
||||
{% else %}
|
||||
{{ line.event_date|date:"d/m/Y" }} - {{ line.label }}
|
||||
<br />
|
||||
{% if line.agenda %}
|
||||
<i>{{ line.agenda.label }}</i>
|
||||
<br />
|
||||
{% endif %}
|
||||
{% endifchanged %}
|
||||
{% else %}
|
||||
{{ line.event_date|date:"d/m/Y" }} - {{ line.label }}
|
||||
<br />
|
||||
{% if line.agenda %}
|
||||
<i>{{ line.agenda.label }}</i>
|
||||
<br />
|
||||
{% endif %}
|
||||
{% if line.details.check_type_label %}
|
||||
{{ line.details.check_type_label }}
|
||||
{% elif line.details.status == 'absence' %}
|
||||
{% trans "Absence" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="details">
|
||||
{% if line.details.dates and not line.agenda.partial_bookings %}
|
||||
{% for d in line.details.dates %}{{ d|parse_date|date:"Dd" }}{% if not forloop.last %}, {% endif %}{% endfor %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if line.details.check_type_label or line.details.status != 'absence' %}
|
||||
<td class="amount unit-amount">{% blocktrans with amount=line.unit_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
|
||||
<td class="quantity">{{ line.quantity|floatformat }}</td>
|
||||
<td class="amount total-amount">{% blocktrans with amount=line.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
|
||||
{% endif %}
|
||||
{% if line.details.check_type_label %}
|
||||
{{ line.details.check_type_label }}
|
||||
{% elif line.details.status == 'absence' %}
|
||||
{% trans "Absence" %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="grand-total">
|
||||
<td class="grand-total-intro" colspan="4">
|
||||
{% trans "Total amount to be paid before:" %} <strong id="payment-deadline">{{ invoice.date_payment_deadline }}</strong>
|
||||
</td>
|
||||
<td class="details">
|
||||
{% if line.details.dates and not line.agenda.partial_bookings %}
|
||||
{% for d in line.details.dates %}{{ d|parse_date|date:"Dd" }}{% if not forloop.last %}, {% endif %}{% endfor %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if line.details.check_type_label or line.details.status != 'absence' %}
|
||||
<td class="amount unit-amount">{% blocktrans with amount=line.unit_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
|
||||
<td class="quantity">{{ line.quantity|floatformat }}</td>
|
||||
<td class="amount total-amount">{% blocktrans with amount=line.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
|
||||
{% endif %}
|
||||
<td class="amount total-amount">{% blocktrans with amount=invoice.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<table id="total">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Payment deadline" %}</th>
|
||||
<th>{% trans "Total due" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ invoice.date_payment_deadline }}</td>
|
||||
<td>{% blocktrans with amount=invoice.total_amount|floatformat:"2" %}{{ amount }}€{% endblocktrans %}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
{% if regie.invoice_custom_text %}
|
||||
<div id="regie-custom-text">
|
||||
{{ regie.invoice_custom_text|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
{% extends "lingo/invoicing/manager_regie_list.html" %}
|
||||
{% load i18n gadjo %}
|
||||
|
||||
{% block extrascripts %}
|
||||
{{ block.super }}
|
||||
{% include "lingo/includes/ckeditor-js.html" only %}
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href="{% url 'lingo-manager-invoicing-regie-detail' regie.pk %}">{{ regie }}</a>
|
||||
<a href="{% url 'lingo-manager-invoicing-regie-invoices-edit' regie.pk %}">{% trans "Invoices Settings" %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans "Invoices Settings" %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form|with_template }}
|
||||
<div class="buttons">
|
||||
<button>{% trans "Submit" %}</button>
|
||||
{% if object.pk %}
|
||||
<a class="cancel" href="{% url 'lingo-manager-invoicing-regie-detail' regie.pk %}">{% trans 'Cancel' %}</a>
|
||||
{% else %}
|
||||
<a class="cancel" href="{% url 'lingo-manager-invoicing-regie-list' %}">{% trans 'Cancel' %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
<li><a rel="popup" href="{% url 'lingo-manager-invoicing-config-import' %}">{% trans 'Import' %}</a></li>
|
||||
<li><a rel="popup" href="{% url 'lingo-manager-invoicing-config-export' %}" data-autoclose-dialog="true">{% trans 'Export' %}</a></li>
|
||||
<li><a href="{% url 'lingo-manager-invoicing-payer-list' %}">{% trans "Payers" %}</a></li>
|
||||
<li><a href="{% url 'lingo-manager-invoicing-appearance-settings' %}">{% trans "Appearance Settings" %}</a></li>
|
||||
</ul>
|
||||
<a rel="popup" href="{% url 'lingo-manager-invoicing-regie-add' %}">{% trans 'New regie' %}</a>
|
||||
</span>
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
<button aria-controls="panel-counters" aria-selected="false" id="tab-counters" role="tab" tabindex="-1">{% trans "Counters" %}</button>
|
||||
<button aria-controls="panel-usage" aria-selected="false" id="tab-usage" role="tab" tabindex="-1">{% trans "Used in agendas" %}</button>
|
||||
<button aria-controls="panel-payment-types" aria-selected="false" id="tab-payment-types" role="tab" tabindex="-1">{% trans "Payment types" %}</button>
|
||||
<button aria-controls="panel-invoices" aria-selected="false" id="tab-invoices" role="tab" tabindex="-1">{% trans "Invoices" %}</button>
|
||||
</div>
|
||||
<div class="pk-tabs--container">
|
||||
|
||||
|
@ -95,6 +96,16 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div aria-labelledby="tab-invoices" hidden="" id="panel-invoices" role="tabpanel" tabindex="0">
|
||||
<dl>
|
||||
<dt><b>{% trans "Custom text:" %}</b></dt>
|
||||
<dd><div>{{ regie.invoice_custom_text|default:"-"|safe }}</div></dd>
|
||||
</dl>
|
||||
<div class="panel--buttons">
|
||||
<a class="pk-button" href="{% url 'lingo-manager-invoicing-regie-invoices-edit' pk=regie.pk %}">{% trans "Edit" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
from django.urls import path
|
||||
|
||||
from .views import appearance as appearance_views
|
||||
from .views import campaign as campaign_views
|
||||
from .views import home as home_views
|
||||
from .views import payer as payer_views
|
||||
|
@ -26,6 +27,11 @@ urlpatterns = [
|
|||
path('import/', home_views.config_import, name='lingo-manager-invoicing-config-import'),
|
||||
path('export/', home_views.config_export, name='lingo-manager-invoicing-config-export'),
|
||||
path('regies/', regie_views.regies_list, name='lingo-manager-invoicing-regie-list'),
|
||||
path(
|
||||
'appearance/',
|
||||
appearance_views.appearance_settings,
|
||||
name='lingo-manager-invoicing-appearance-settings',
|
||||
),
|
||||
path(
|
||||
'regie/add/',
|
||||
regie_views.regie_add,
|
||||
|
@ -51,6 +57,11 @@ urlpatterns = [
|
|||
regie_views.regie_counters_edit,
|
||||
name='lingo-manager-invoicing-regie-counters-edit',
|
||||
),
|
||||
path(
|
||||
'regie/<int:pk>/edit/invoices/',
|
||||
regie_views.regie_invoices_edit,
|
||||
name='lingo-manager-invoicing-regie-invoices-edit',
|
||||
),
|
||||
path(
|
||||
'regie/<int:pk>/delete/',
|
||||
regie_views.regie_delete,
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# lingo - payment and billing system
|
||||
# Copyright (C) 2023 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic import UpdateView
|
||||
|
||||
from lingo.invoicing.models import AppearanceSettings
|
||||
|
||||
|
||||
class AppearanceSettingsView(UpdateView):
|
||||
model = AppearanceSettings
|
||||
success_url = reverse_lazy('lingo-manager-invoicing-regie-list')
|
||||
template_name = 'lingo/invoicing/appearance_settings_form.html'
|
||||
fields = '__all__'
|
||||
|
||||
def get_object(self):
|
||||
return AppearanceSettings.singleton()
|
||||
|
||||
|
||||
appearance_settings = AppearanceSettingsView.as_view()
|
|
@ -143,6 +143,20 @@ class RegieCountersEditView(UpdateView):
|
|||
regie_counters_edit = RegieCountersEditView.as_view()
|
||||
|
||||
|
||||
class RegieInvoicesEditView(UpdateView):
|
||||
template_name = 'lingo/invoicing/manager_regie_invoices_form.html'
|
||||
model = Regie
|
||||
fields = [
|
||||
'invoice_custom_text',
|
||||
]
|
||||
|
||||
def get_success_url(self):
|
||||
return '%s#open:invoices' % reverse('lingo-manager-invoicing-regie-parameters', args=[self.object.pk])
|
||||
|
||||
|
||||
regie_invoices_edit = RegieInvoicesEditView.as_view()
|
||||
|
||||
|
||||
class RegieDeleteView(DeleteView):
|
||||
template_name = 'lingo/manager_confirm_delete.html'
|
||||
model = Regie
|
||||
|
|
|
@ -33,5 +33,6 @@ class PDFMixin:
|
|||
html = HTML(string=result)
|
||||
pdf = html.write_pdf()
|
||||
response = HttpResponse(pdf, content_type='application/pdf')
|
||||
response['Content-Disposition'] = 'attachment; filename="%s.pdf"' % self.get_filename()
|
||||
if 'inline' not in request.GET:
|
||||
response['Content-Disposition'] = 'attachment; filename="%s.pdf"' % self.get_filename()
|
||||
return response
|
||||
|
|
|
@ -152,3 +152,8 @@ table.invoicing-element-list {
|
|||
ul.objects-list li span.badge.badge-success {
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
.django-ckeditor-widget {
|
||||
display: block !important;
|
||||
max-width: 80em;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
# lingo - payment and billing system
|
||||
# Copyright (C) 2022-2023 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ckeditor.views
|
||||
import ckeditor.widgets
|
||||
from django.forms.utils import flatatt
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import get_language
|
||||
|
||||
|
||||
def ckeditor_render(self, name, value, attrs=None, renderer=None):
|
||||
if value is None:
|
||||
value = ''
|
||||
final_attrs = {'name': name}
|
||||
if getattr(self, 'attrs', None):
|
||||
final_attrs.update(self.attrs)
|
||||
if attrs:
|
||||
final_attrs.update(attrs)
|
||||
if not self.config.get('language'):
|
||||
self.config['language'] = get_language()
|
||||
|
||||
# Force to text to evaluate possible lazy objects
|
||||
external_plugin_resources = [
|
||||
[force_str(a), force_str(b), force_str(c)] for a, b, c in self.external_plugin_resources
|
||||
]
|
||||
|
||||
return mark_safe(
|
||||
render_to_string(
|
||||
'ckeditor/widget.html',
|
||||
{
|
||||
'final_attrs': flatatt(final_attrs),
|
||||
'value': conditional_escape(force_str(value)),
|
||||
'id': final_attrs['id'],
|
||||
'config': ckeditor.widgets.json_encode(self.config),
|
||||
'external_plugin_resources': ckeditor.widgets.json_encode(external_plugin_resources),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
ckeditor.widgets.CKEditorWidget.render = ckeditor_render
|
|
@ -52,6 +52,7 @@ INSTALLED_APPS = (
|
|||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.humanize',
|
||||
'ckeditor',
|
||||
'eopayment',
|
||||
'gadjo',
|
||||
'rest_framework',
|
||||
|
@ -202,6 +203,29 @@ DEBUG_TOOLBAR_CONFIG = {'SHOW_TOOLBAR_CALLBACK': debug_show_toolbar}
|
|||
|
||||
REST_FRAMEWORK = {'EXCEPTION_HANDLER': 'lingo.api.utils.exception_handler'}
|
||||
|
||||
CKEDITOR_UPLOAD_PATH = 'uploads/'
|
||||
CKEDITOR_IMAGE_BACKEND = 'pillow'
|
||||
|
||||
CKEDITOR_CONFIGS = {
|
||||
'default': {
|
||||
'allowedContent': False,
|
||||
'extraPlugins': 'stylescombo',
|
||||
'removePlugins': 'stylesheetparser',
|
||||
'toolbar_Own': [
|
||||
['Styles'],
|
||||
['Bold', 'Italic'],
|
||||
['NumberedList', 'BulletedList'],
|
||||
['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'],
|
||||
['Link', 'Unlink'],
|
||||
['HorizontalRule'],
|
||||
],
|
||||
'toolbar': 'Own',
|
||||
'resize_enabled': False,
|
||||
'width': '100%',
|
||||
'height': 150,
|
||||
},
|
||||
}
|
||||
|
||||
local_settings_file = os.environ.get(
|
||||
'LINGO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')
|
||||
)
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{% load i18n static %}
|
||||
<script src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script>
|
||||
<script>
|
||||
CKEDITOR.stylesSet.add('default',
|
||||
[
|
||||
{name: '{% trans "Paragraph" %}', element: 'p', styles: {'font-size': 'unset'}},
|
||||
{name: '{% trans "Small paragraph" %}', element: 'p', styles: {'font-size': '80%'}},
|
||||
]
|
||||
)
|
||||
</script>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
# lingo - payment and billing system
|
||||
# Copyright (C) 2022-2023 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ckeditor.fields
|
||||
from django.conf import settings
|
||||
|
||||
import lingo.monkeypatch # noqa pylint: disable=unused-import
|
||||
|
||||
|
||||
class RichTextField(ckeditor.fields.RichTextField):
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {
|
||||
'form_class': RichTextFormField,
|
||||
'config_name': self.config_name,
|
||||
'extra_plugins': self.extra_plugins,
|
||||
'external_plugin_resources': self.external_plugin_resources,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return super().formfield(**defaults)
|
||||
|
||||
|
||||
class RichTextFormField(ckeditor.fields.RichTextFormField):
|
||||
def clean(self, value):
|
||||
value = super().clean(value)
|
||||
if settings.LANGUAGE_CODE.startswith('fr-'):
|
||||
# apply some typographic rules
|
||||
value = value.replace('« ', '«\u202f')
|
||||
value = value.replace('« ', '«\u202f')
|
||||
value = value.replace(' »', '\u202f»')
|
||||
value = value.replace(' »', '\u202f»')
|
||||
value = value.replace(' :', '\u00a0:')
|
||||
value = value.replace(' ;', '\u202f;')
|
||||
value = value.replace(' !', '\u202f!')
|
||||
value = value.replace(' ?', '\u202f?')
|
||||
return value
|
1
setup.py
1
setup.py
|
@ -161,6 +161,7 @@ setup(
|
|||
],
|
||||
install_requires=[
|
||||
'django>=3.2, <3.3',
|
||||
'django-ckeditor<4.5.4',
|
||||
'gadjo>=0.53',
|
||||
'requests',
|
||||
'eopayment>=1.60',
|
||||
|
|
|
@ -1993,23 +1993,23 @@ def test_invoice_pdf(app, admin_user, draft):
|
|||
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/invoice/%s/pdf/?html'
|
||||
% (regie.pk, campaign.pk, pool.pk, invoice.pk)
|
||||
)
|
||||
assert resp.pyquery('h1').text() == 'Invoice from 01/09/2022 to 30/09/2022'
|
||||
assert resp.pyquery('address#from').text() == 'Foo'
|
||||
assert resp.pyquery('#invoice-label').text() == 'Invoice from 01/09/2022 to 30/09/2022'
|
||||
assert resp.pyquery('#regie-label').text() == 'Foo'
|
||||
assert resp.pyquery('address#to').text() == 'First1 Name1'
|
||||
if draft:
|
||||
assert resp.pyquery('dl#informations').text() == 'Invoice number\nTEMPORARY-%s\nDate\n%s' % (
|
||||
assert resp.pyquery('dl#informations').text() == 'Invoice number:\nTEMPORARY-%s\nDate:\n%s' % (
|
||||
invoice.pk,
|
||||
date_format(localtime(invoice.created_at), 'DATE_FORMAT'),
|
||||
)
|
||||
else:
|
||||
assert resp.pyquery('dl#informations').text() == 'Invoice number\nF%02d-%s-0000001\nDate\n%s' % (
|
||||
assert resp.pyquery('dl#informations').text() == 'Invoice number:\nF%02d-%s-0000001\nDate:\n%s' % (
|
||||
regie.pk,
|
||||
invoice.created_at.strftime('%y-%m'),
|
||||
date_format(localtime(invoice.created_at), 'DATE_FORMAT'),
|
||||
)
|
||||
assert len(resp.pyquery('h2 + table#lines')) == 2
|
||||
assert resp.pyquery('h2')[0].text == 'User1 Name1'
|
||||
assert resp.pyquery('h2')[1].text == 'User2 Name2'
|
||||
assert len(resp.pyquery('h2.user-name + table#lines')) == 2
|
||||
assert resp.pyquery('h2.user-name')[0].text == 'User1 Name1'
|
||||
assert resp.pyquery('h2.user-name')[1].text == 'User2 Name2'
|
||||
assert len(resp.pyquery('table#lines')) == 2
|
||||
tables = resp.pyquery('table#lines')
|
||||
assert [PyQuery(tr).text() for tr in PyQuery(tables).find('thead tr')] == [
|
||||
|
@ -2023,7 +2023,8 @@ def test_invoice_pdf(app, admin_user, draft):
|
|||
assert [PyQuery(tr).text() for tr in PyQuery(tables[1]).find('tbody tr')] == [
|
||||
'02/09/2022 - Label 12\n\n2.00€\n1\n2.00€',
|
||||
]
|
||||
assert resp.pyquery('table#total tbody').text() == 'Oct. 31, 2022\n6.20€'
|
||||
assert resp.pyquery('#payment-deadline').text() == 'Oct. 31, 2022'
|
||||
assert resp.pyquery('tfoot .total-amount').text() == '6.20€'
|
||||
|
||||
resp = app.get(
|
||||
'/manage/invoicing/regie/%s/campaign/%s/pool/%s/invoice/%s/pdf/?html'
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import base64
|
||||
import datetime
|
||||
import decimal
|
||||
from urllib.parse import urlparse
|
||||
|
@ -8,10 +9,12 @@ from django.urls import reverse
|
|||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import localtime, now
|
||||
from pyquery import PyQuery
|
||||
from webtest import Upload
|
||||
|
||||
from lingo.agendas.models import Agenda
|
||||
from lingo.invoicing.models import (
|
||||
DEFAULT_PAYMENT_TYPES,
|
||||
AppearanceSettings,
|
||||
Campaign,
|
||||
Counter,
|
||||
DraftJournalLine,
|
||||
|
@ -217,6 +220,45 @@ def test_manager_invoicing_regie_counters_edit(app, admin_user):
|
|||
assert resp.location.endswith('/manage/invoicing/regie/%s/parameters/#open:counters' % regie.pk)
|
||||
|
||||
|
||||
def test_manager_invoicing_appearance_settings(app, admin_user, settings):
|
||||
app = login(app)
|
||||
regie = Regie.objects.create(label='Foo', description='foo description')
|
||||
resp = app.get(reverse('lingo-manager-invoicing-regie-list'))
|
||||
resp = resp.click('Appearance Settings')
|
||||
resp.form['logo'] = Upload(
|
||||
'test.png',
|
||||
base64.decodebytes(
|
||||
b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVQI12NoAAAAggCB3UNq9AAAAABJRU5ErkJggg=='
|
||||
),
|
||||
'image/png',
|
||||
)
|
||||
resp.form['address'] = '<p>Foo bar<br>Streetname</p>'
|
||||
resp.form['extra_info'] = '<p>Opening hours...</p>'
|
||||
resp = resp.form.submit('submit').follow()
|
||||
appearance_settings = AppearanceSettings.singleton()
|
||||
assert appearance_settings.logo.name == 'logo/test.png'
|
||||
assert appearance_settings.address == '<p>Foo bar<br>Streetname</p>'
|
||||
assert appearance_settings.extra_info == '<p>Opening hours...</p>'
|
||||
resp = resp.click('Appearance Settings')
|
||||
assert resp.form['address'].value == '<p>Foo bar<br>Streetname</p>'
|
||||
|
||||
# regie settings
|
||||
resp = app.get(reverse('lingo-manager-invoicing-regie-invoices-edit', kwargs={'pk': regie.pk}))
|
||||
resp.form.set('invoice_custom_text', '<p>custom text</p>')
|
||||
resp = resp.form.submit()
|
||||
regie.refresh_from_db()
|
||||
assert regie.invoice_custom_text == '<p>custom text</p>'
|
||||
assert resp.location.endswith('/manage/invoicing/regie/%s/parameters/#open:invoices' % regie.pk)
|
||||
|
||||
# check French typography fixes
|
||||
settings.LANGUAGE_CODE = 'fr-fr'
|
||||
resp = app.get(reverse('lingo-manager-invoicing-regie-invoices-edit', kwargs={'pk': regie.pk}))
|
||||
resp.form.set('invoice_custom_text', '<p>custom : text</p>')
|
||||
resp = resp.form.submit()
|
||||
regie.refresh_from_db()
|
||||
assert regie.invoice_custom_text == '<p>custom\u00a0: text</p>'
|
||||
|
||||
|
||||
def test_manager_invoicing_regie_delete(app, admin_user):
|
||||
app = login(app)
|
||||
group = Group.objects.create(name='role-foo')
|
||||
|
@ -1166,17 +1208,17 @@ def test_regie_invoice_pdf(app, admin_user):
|
|||
|
||||
app = login(app)
|
||||
resp = app.get('/manage/invoicing/regie/%s/invoice/%s/pdf/?html' % (regie.pk, invoice.pk))
|
||||
assert resp.pyquery('h1').text() == 'Invoice from 01/09/2022 to 30/09/2022'
|
||||
assert resp.pyquery('address#from').text() == 'Foo'
|
||||
assert resp.pyquery('#invoice-label').text() == 'Invoice from 01/09/2022 to 30/09/2022'
|
||||
assert resp.pyquery('#regie-label').text() == 'Foo'
|
||||
assert resp.pyquery('address#to').text() == 'First1 Name1'
|
||||
assert resp.pyquery('dl#informations').text() == 'Invoice number\nF%02d-%s-0000001\nDate\n%s' % (
|
||||
assert resp.pyquery('dl#informations').text() == 'Invoice number:\nF%02d-%s-0000001\nDate:\n%s' % (
|
||||
regie.pk,
|
||||
invoice.created_at.strftime('%y-%m'),
|
||||
date_format(localtime(invoice.created_at), 'DATE_FORMAT'),
|
||||
)
|
||||
assert len(resp.pyquery('h2 + table#lines')) == 2
|
||||
assert resp.pyquery('h2')[0].text == 'User1 Name1'
|
||||
assert resp.pyquery('h2')[1].text == 'User2 Name2'
|
||||
assert len(resp.pyquery('h2.user-name + table#lines')) == 2
|
||||
assert resp.pyquery('h2.user-name')[0].text == 'User1 Name1'
|
||||
assert resp.pyquery('h2.user-name')[1].text == 'User2 Name2'
|
||||
assert len(resp.pyquery('table#lines')) == 2
|
||||
tables = resp.pyquery('table#lines')
|
||||
assert [PyQuery(tr).text() for tr in PyQuery(tables).find('thead tr')] == [
|
||||
|
@ -1190,7 +1232,8 @@ def test_regie_invoice_pdf(app, admin_user):
|
|||
assert [PyQuery(tr).text() for tr in PyQuery(tables[1]).find('tbody tr')] == [
|
||||
'02/09/2022 - Label 12\n\n2.00€\n1\n2.00€',
|
||||
]
|
||||
assert resp.pyquery('table#total tbody').text() == 'Oct. 31, 2022\n6.20€'
|
||||
assert resp.pyquery('#payment-deadline').text() == 'Oct. 31, 2022'
|
||||
assert resp.pyquery('tfoot .total-amount').text() == '6.20€'
|
||||
|
||||
resp = app.get('/manage/invoicing/regie/%s//invoice/%s/pdf/?html' % (0, invoice.pk), status=404)
|
||||
resp = app.get('/manage/invoicing/regie/%s//invoice/%s/pdf/?html' % (regie.pk, 0), status=404)
|
||||
|
@ -1199,6 +1242,14 @@ def test_regie_invoice_pdf(app, admin_user):
|
|||
'/manage/invoicing/regie/%s/invoice/%s/pdf/?html' % (other_regie.pk, invoice.pk), status=404
|
||||
)
|
||||
|
||||
appearance_settings = AppearanceSettings.singleton()
|
||||
appearance_settings.address = '<p>Foo bar<br>Streetname</p>'
|
||||
appearance_settings.extra_info = '<p>Opening hours...</p>'
|
||||
appearance_settings.save()
|
||||
resp = app.get('/manage/invoicing/regie/%s/invoice/%s/pdf/?html' % (regie.pk, invoice.pk))
|
||||
assert appearance_settings.address in resp.text
|
||||
assert appearance_settings.extra_info in resp.text
|
||||
|
||||
|
||||
def test_regie_invoice_payments_pdf(app, admin_user):
|
||||
regie = Regie.objects.create(label='Foo')
|
||||
|
|
2
tox.ini
2
tox.ini
|
@ -32,6 +32,7 @@ deps =
|
|||
django-mellon>=1.13
|
||||
django-filter>=2.4,<2.5
|
||||
git+https://git.entrouvert.org/publik-django-templatetags.git
|
||||
git+https://git.entrouvert.org/entrouvert/debian-django-ckeditor.git
|
||||
pre-commit
|
||||
commands =
|
||||
./getlasso3.sh
|
||||
|
@ -59,6 +60,7 @@ deps =
|
|||
pyquery
|
||||
psycopg2-binary
|
||||
git+https://git.entrouvert.org/publik-django-templatetags.git
|
||||
git+https://git.entrouvert.org/entrouvert/debian-django-ckeditor.git
|
||||
commands =
|
||||
./getlasso3.sh
|
||||
./pylint.sh lingo/
|
||||
|
|
Loading…
Reference in New Issue