Projet

Général

Profil

0001-start-invoicing-app-65359.patch

Emmanuel Cazenave, 21 septembre 2022 11:17

Télécharger (22,9 ko)

Voir les différences:

Subject: [PATCH] start invoicing app (#65359)

 lingo/invoicing/__init__.py                   |   0
 lingo/invoicing/migrations/0001_initial.py    |  51 +++++++
 lingo/invoicing/migrations/__init__.py        |   0
 lingo/invoicing/models.py                     |  54 +++++++
 .../lingo/invoicing/manager_home.html         |  22 +++
 .../lingo/invoicing/manager_regie_common.html |  12 ++
 .../lingo/invoicing/manager_regie_detail.html |  28 ++++
 .../lingo/invoicing/manager_regie_form.html   |  31 ++++
 .../lingo/invoicing/manager_regie_list.html   |  34 +++++
 lingo/invoicing/urls.py                       |  44 ++++++
 lingo/invoicing/views.py                      |  83 ++++++++++
 .../templates/lingo/manager_homepage.html     |   1 +
 lingo/settings.py                             |   1 +
 lingo/urls.py                                 |   2 +
 tests/invoicing/test_manager.py               | 144 ++++++++++++++++++
 15 files changed, 507 insertions(+)
 create mode 100644 lingo/invoicing/__init__.py
 create mode 100644 lingo/invoicing/migrations/0001_initial.py
 create mode 100644 lingo/invoicing/migrations/__init__.py
 create mode 100644 lingo/invoicing/models.py
 create mode 100644 lingo/invoicing/templates/lingo/invoicing/manager_home.html
 create mode 100644 lingo/invoicing/templates/lingo/invoicing/manager_regie_common.html
 create mode 100644 lingo/invoicing/templates/lingo/invoicing/manager_regie_detail.html
 create mode 100644 lingo/invoicing/templates/lingo/invoicing/manager_regie_form.html
 create mode 100644 lingo/invoicing/templates/lingo/invoicing/manager_regie_list.html
 create mode 100644 lingo/invoicing/urls.py
 create mode 100644 lingo/invoicing/views.py
 create mode 100644 tests/invoicing/test_manager.py
lingo/invoicing/migrations/0001_initial.py
1
# Generated by Django 2.2.26 on 2022-09-21 08:59
2

  
3
import django.db.models.deletion
4
from django.db import migrations, models
5

  
6

  
7
class Migration(migrations.Migration):
8

  
9
    initial = True
10

  
11
    dependencies = [
12
        ('auth', '0011_update_proxy_permissions'),
13
    ]
14

  
15
    operations = [
16
        migrations.CreateModel(
17
            name='Regie',
18
            fields=[
19
                (
20
                    'id',
21
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
22
                ),
23
                ('label', models.CharField(max_length=150, verbose_name='Label')),
24
                ('slug', models.SlugField(max_length=160, unique=True, verbose_name='Identifier')),
25
                (
26
                    'description',
27
                    models.TextField(
28
                        blank=True,
29
                        help_text='Optional regie description.',
30
                        null=True,
31
                        verbose_name='Description',
32
                    ),
33
                ),
34
                (
35
                    'cashier_role',
36
                    models.ForeignKey(
37
                        blank=True,
38
                        default=None,
39
                        null=True,
40
                        on_delete=django.db.models.deletion.SET_NULL,
41
                        related_name='+',
42
                        to='auth.Group',
43
                        verbose_name='Cashier Role',
44
                    ),
45
                ),
46
            ],
47
            options={
48
                'ordering': ['label'],
49
            },
50
        ),
51
    ]
lingo/invoicing/models.py
1
# lingo - payment and billing system
2
# Copyright (C) 2022  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.contrib.auth.models import Group
18
from django.db import models
19
from django.utils.text import slugify
20
from django.utils.translation import gettext_lazy as _
21

  
22
from lingo.utils.misc import generate_slug
23

  
24

  
25
class Regie(models.Model):
26
    label = models.CharField(_('Label'), max_length=150)
27
    slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
28
    description = models.TextField(
29
        _('Description'), null=True, blank=True, help_text=_('Optional regie description.')
30
    )
31
    cashier_role = models.ForeignKey(
32
        Group,
33
        blank=True,
34
        null=True,
35
        default=None,
36
        related_name='+',
37
        verbose_name=_('Cashier Role'),
38
        on_delete=models.SET_NULL,
39
    )
40

  
41
    class Meta:
42
        ordering = ['label']
43

  
44
    def __str__(self):
45
        return self.label
46

  
47
    def save(self, *args, **kwargs):
48
        if not self.slug:
49
            self.slug = generate_slug(self)
50
        super().save(*args, **kwargs)
51

  
52
    @property
53
    def base_slug(self):
54
        return slugify(self.label)
lingo/invoicing/templates/lingo/invoicing/manager_home.html
1
{% extends "lingo/manager_base.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="{% url 'lingo-manager-invoicing-home' %}">{% trans 'Invoicing' %}</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
<h2>{% trans 'Invoicing' %}</h2>
11
{% endblock %}
12

  
13
{% block content %}
14
<div id="lingo-manager-main">
15
  <div class="fx-grid--t3">
16
      <a class="button button-paragraph" href="{% url 'lingo-manager-invoicing-regie-list' %}">
17
      {% trans "Regies" %}
18
      <p>{% trans "Invoicing regies." %}</p>
19
    </a>
20
  </div>
21
</div>
22
{% endblock %}
lingo/invoicing/templates/lingo/invoicing/manager_regie_common.html
1
{% extends "lingo/invoicing/manager_home.html" %}
2
{% load i18n %}
3

  
4
{% block appbar %}
5
<h2>{% if regie %}{% trans 'Regie' %} - {{regie}} {% else %}{% trans 'Regies' %}{% endif %}</h2>
6
{% endblock %}
7

  
8

  
9
{% block breadcrumb %}
10
{{ block.super }}
11
<a href="{% url 'lingo-manager-invoicing-regie-list' %}">{% trans "Regies" %}</a>
12
{% endblock %}
lingo/invoicing/templates/lingo/invoicing/manager_regie_detail.html
1
{% extends "lingo/invoicing/manager_regie_common.html" %}
2
{% load i18n gadjo %}
3

  
4
{% block breadcrumb %}
5
  {{ block.super }}
6
  <a href="{% url 'lingo-manager-invoicing-regie-detail' regie.pk %}">{{ object }}</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
  {{ block.super }}
11
  <span class="actions">
12
    <a href="{% url 'lingo-manager-invoicing-regie-delete' pk=regie.pk %}" rel="popup">{% trans "Delete" %}</a>
13
    <a href="{% url 'lingo-manager-invoicing-regie-edit' pk=regie.pk %}" rel="popup">{% trans "Edit" %}</a>
14
  </span>
15
  {% endblock %}
16

  
17
{% block content %}
18
{% if regie.description %}
19
<div class="bo-block">{{regie.description}}</div>
20
{% endif %}
21
<div class="bo-block">
22
  <h3>{% trans "Parameters" %}</h3>
23
  <ul>
24
    <li>{% trans "slug" %}&nbsp;: {{regie.slug}}</li>
25
    <li>{% trans "cashier role" %}&nbsp;: {{regie.cashier_role}}</li>
26
  </ul>
27
</div>
28
{% endblock %}
lingo/invoicing/templates/lingo/invoicing/manager_regie_form.html
1
{% extends "lingo/invoicing/manager_regie_common.html" %}
2
{% load i18n gadjo %}
3

  
4
{% block breadcrumb %}
5
  {{ block.super }}
6
  {% if object.pk %}
7
  <a href="{% url 'lingo-manager-invoicing-regie-detail' regie.pk %}">{{ regie }}</a>
8
  <a href="{% url 'lingo-manager-invoicing-regie-edit' regie.pk %}">{% trans "Edit" %}</a>
9
  {% else %}
10
  <a href="{% url 'lingo-manager-invoicing-regie-add' %}">{% trans "New regie" %}</a>
11
  {% endif %}
12
{% endblock %}
13

  
14
{% block appbar %}
15
{% if regie.pk %}
16
<h2>{% trans "Edit regie" %} - {{regie}}</h2>
17
{% else %}
18
<h2>{% trans "New regie" %}</h2>
19
{% endif %}
20
{% endblock %}
21

  
22
{% block content %}
23
  <form method="post" enctype="multipart/form-data">
24
    {% csrf_token %}
25
    {{ form|with_template }}
26
    <div class="buttons">
27
      <button>{% trans "Submit" %}</button>
28
      <a class="cancel" href="{{cancel_url}}">{% trans 'Cancel' %}</a>
29
    </div>
30
  </form>
31
{% endblock %}
lingo/invoicing/templates/lingo/invoicing/manager_regie_list.html
1
{% extends "lingo/invoicing/manager_regie_common.html" %}
2
{% load i18n %}
3

  
4
{% block appbar %}
5
<h2>{% trans 'Regies' %}</h2>
6
<span class="actions">
7
  <a rel="popup" href="{% url 'lingo-manager-invoicing-regie-add' %}">{% trans 'New regie' %}</a>
8
</span>
9
{% endblock %}
10

  
11
{% block content %}
12
{% if object_list %}
13
<div>
14
  <ul class="objects-list single-links">
15
    {% for regie in object_list %}
16
    <li>
17
      <a href="{% url 'lingo-manager-invoicing-regie-detail' pk=regie.pk %}">
18
        {{ regie.label }}
19
        <span class="extra-info"> [{% trans "identifier:" %} {{ regie.slug }}]</span>
20
      </a>
21
    </li>
22
    {% endfor %}
23
  </ul>
24
</div>
25
{% else %}
26

  
27
<div class="big-msg-info">
28
  {% blocktrans %}
29
  This site doesn't have any regie yet. Click on the "New" button in the top
30
  right of the page to add a first one.
31
  {% endblocktrans %}
32
</div>
33
{% endif %}
34
{% endblock %}
lingo/invoicing/urls.py
1
# lingo - payment and billing system
2
# Copyright (C) 2022  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.urls import path
18

  
19
from . import views
20

  
21
urlpatterns = [
22
    path('', views.home, name='lingo-manager-invoicing-home'),
23
    path('regies/', views.regies_list, name='lingo-manager-invoicing-regie-list'),
24
    path(
25
        'regie/add/',
26
        views.regie_add,
27
        name='lingo-manager-invoicing-regie-add',
28
    ),
29
    path(
30
        'regie/<int:pk>/',
31
        views.regie_detail,
32
        name='lingo-manager-invoicing-regie-detail',
33
    ),
34
    path(
35
        'regie/<int:pk>/edit/',
36
        views.regie_edit,
37
        name='lingo-manager-invoicing-regie-edit',
38
    ),
39
    path(
40
        'regie/<int:pk>/delete/',
41
        views.regie_delete,
42
        name='lingo-manager-invoicing-regie-delete',
43
    ),
44
]
lingo/invoicing/views.py
1
# lingo - payment and billing system
2
# Copyright (C) 2022  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.urls import reverse
18
from django.views.generic import CreateView, DeleteView, DetailView, ListView, TemplateView, UpdateView
19

  
20
from lingo.invoicing.models import Regie
21

  
22

  
23
class HomeView(TemplateView):
24
    template_name = 'lingo/invoicing/manager_home.html'
25

  
26

  
27
home = HomeView.as_view()
28

  
29

  
30
class RegiesListView(ListView):
31
    template_name = 'lingo/invoicing/manager_regie_list.html'
32
    model = Regie
33

  
34

  
35
regies_list = RegiesListView.as_view()
36

  
37

  
38
class RegieAddView(CreateView):
39
    template_name = 'lingo/invoicing/manager_regie_form.html'
40
    model = Regie
41
    fields = ['label', 'description', 'cashier_role']
42

  
43
    def get_success_url(self):
44
        return reverse('lingo-manager-invoicing-regie-detail', args=[self.object.pk])
45

  
46

  
47
regie_add = RegieAddView.as_view()
48

  
49

  
50
class RegieDetailView(DetailView):
51
    template_name = 'lingo/invoicing/manager_regie_detail.html'
52
    model = Regie
53

  
54
    def get_context_data(self, **kwargs):
55
        context = super().get_context_data(**kwargs)
56
        context['regie'] = self.object
57
        return context
58

  
59

  
60
regie_detail = RegieDetailView.as_view()
61

  
62

  
63
class RegieEditView(UpdateView):
64
    template_name = 'lingo/invoicing/manager_regie_form.html'
65
    model = Regie
66
    fields = ['label', 'description', 'cashier_role']
67

  
68
    def get_success_url(self):
69
        return reverse('lingo-manager-invoicing-regie-detail', args=[self.object.pk])
70

  
71

  
72
regie_edit = RegieEditView.as_view()
73

  
74

  
75
class RegieDeleteView(DeleteView):
76
    template_name = 'lingo/manager_confirm_delete.html'
77
    model = Regie
78

  
79
    def get_success_url(self):
80
        return reverse('lingo-manager-invoicing-regie-list')
81

  
82

  
83
regie_delete = RegieDeleteView.as_view()
lingo/manager/templates/lingo/manager_homepage.html
5 5
  <h2>{% trans 'Payments' %}</h2>
6 6
  <span class="actions">
7 7
    <a href="{% url 'lingo-manager-pricing-home' %}">{% trans 'Pricing' context 'pricing' %}</a>
8
    <a href="{% url 'lingo-manager-invoicing-home' %}">{% trans 'Invoicing' %}</a>
8 9
  </span>
9 10
{% endblock %}
lingo/settings.py
57 57
    'rest_framework',
58 58
    'lingo.agendas',
59 59
    'lingo.api',
60
    'lingo.invoicing',
60 61
    'lingo.manager',
61 62
    'lingo.pricing',
62 63
)
lingo/urls.py
20 20
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
21 21

  
22 22
from .api.urls import urlpatterns as lingo_api_urls
23
from .invoicing.urls import urlpatterns as lingo_invoicing_urls
23 24
from .manager.urls import urlpatterns as lingo_manager_urls
24 25
from .pricing.urls import urlpatterns as lingo_pricing_urls
25 26
from .urls_utils import decorated_includes, manager_required
......
28 29
urlpatterns = [
29 30
    url(r'^$', homepage, name='homepage'),
30 31
    url(r'^manage/', decorated_includes(manager_required, include(lingo_manager_urls))),
32
    url(r'^manage/invoicing/', decorated_includes(manager_required, include(lingo_invoicing_urls))),
31 33
    url(r'^manage/pricing/', decorated_includes(manager_required, include(lingo_pricing_urls))),
32 34
    url(r'^api/', include(lingo_api_urls)),
33 35
    url(r'^login/$', login, name='auth_login'),
tests/invoicing/test_manager.py
1
from urllib.parse import urlparse
2

  
3
import pytest
4
from django.contrib.auth.models import Group
5
from django.urls import reverse
6

  
7
from lingo.invoicing.models import Regie
8
from tests.utils import login
9

  
10
pytestmark = pytest.mark.django_db
11

  
12

  
13
def test_manager_home_show_invoicing(app, admin_user):
14
    app = login(app)
15
    resp = app.get('/manage/')
16
    anchor = resp.pyquery('div#appbar span.actions a[href="%s"]' % reverse('lingo-manager-invoicing-home'))
17
    assert anchor.text() == 'Invoicing'
18

  
19

  
20
def test_manager_invoicing_home(app, admin_user):
21
    app = login(app)
22
    resp = app.get(reverse('lingo-manager-invoicing-home'))
23
    h2 = resp.pyquery('div#appbar h2')
24
    assert h2.text() == 'Invoicing'
25
    anchor = resp.pyquery(
26
        'div#lingo-manager-main div a[href="%s"]' % reverse('lingo-manager-invoicing-regie-list')
27
    )
28
    assert anchor.text().startswith('Regies')
29

  
30

  
31
def test_manager_invoicing_regie_list_title(app, admin_user):
32
    app = login(app)
33
    resp = app.get(reverse('lingo-manager-invoicing-regie-list'))
34
    h2 = resp.pyquery('div#appbar h2')
35
    assert h2.text() == 'Regies'
36

  
37

  
38
def test_manager_invoicing_regie_list_empty(app, admin_user):
39
    app = login(app)
40
    resp = app.get(reverse('lingo-manager-invoicing-regie-list'))
41
    msg_info = resp.pyquery('div#content div.big-msg-info')
42
    assert (
43
        msg_info.text()
44
        == "This site doesn't have any regie yet. Click on the \"New\" button in the top right of the page to add a first one."
45
    )
46

  
47

  
48
def test_manager_invoicing_regie_list_add_button(app, admin_user):
49
    app = login(app)
50
    resp = app.get(reverse('lingo-manager-invoicing-regie-list'))
51
    add_button = resp.pyquery('span.actions a[href="%s"]' % reverse('lingo-manager-invoicing-regie-add'))
52
    assert add_button.text() == 'New regie'
53

  
54

  
55
def test_manager_invoicing_regie_list_show_objetcs(app, admin_user):
56
    app = login(app)
57
    regie = Regie.objects.create(label='Foo')
58
    resp = app.get(reverse('lingo-manager-invoicing-regie-list'))
59
    href = resp.pyquery(
60
        'div#content div ul li a[href="%s"]'
61
        % reverse('lingo-manager-invoicing-regie-detail', kwargs={'pk': regie.pk})
62
    )
63
    assert href.text() == 'Foo [identifier: foo]'
64

  
65

  
66
def test_manager_invoicing_regie_add(app, admin_user):
67
    app = login(app)
68
    assert Regie.objects.count() == 0
69
    group = Group.objects.create(name='role-foo')
70
    resp = app.get(reverse('lingo-manager-invoicing-regie-add'))
71
    h2 = resp.pyquery('div#appbar h2')
72
    assert h2.text() == 'New regie'
73
    form = resp.form
74
    form.set('label', 'Foo')
75
    form.set('description', 'foo description')
76
    form.set('cashier_role', group.id)
77
    response = form.submit().follow()
78
    assert Regie.objects.count() == 1
79
    regie = Regie.objects.first()
80
    assert regie.label == 'Foo'
81
    assert regie.slug == 'foo'
82
    assert regie.description == 'foo description'
83
    assert regie.cashier_role == group
84
    assert urlparse(response.request.url).path == reverse(
85
        'lingo-manager-invoicing-regie-detail', kwargs={'pk': regie.pk}
86
    )
87

  
88

  
89
def test_manager_invoicing_regie_detail(app, admin_user):
90
    app = login(app)
91
    group = Group.objects.create(name='role-foo')
92
    regie = Regie.objects.create(label='Foo', description='foo description', cashier_role=group)
93
    resp = app.get(reverse('lingo-manager-invoicing-regie-detail', kwargs={'pk': regie.pk}))
94
    h2 = resp.pyquery('div#appbar h2')
95
    assert h2.text() == 'Regie - Foo'
96
    descr = resp.pyquery('div#content div.bo-block')[0]
97
    assert descr.text == 'foo description'
98
    slug = resp.pyquery('div#content div.bo-block ul li')[0]
99
    assert slug.text == 'slug\xa0: foo'
100
    cashier_role = resp.pyquery('div#content div.bo-block ul li')[1]
101
    assert cashier_role.text == 'cashier role\xa0: role-foo'
102
    edit_button = resp.pyquery(
103
        'span.actions a[href="%s"]' % reverse('lingo-manager-invoicing-regie-edit', kwargs={'pk': regie.pk})
104
    )
105
    assert edit_button.text() == 'Edit'
106
    delete_button = resp.pyquery(
107
        'span.actions a[href="%s"]' % reverse('lingo-manager-invoicing-regie-delete', kwargs={'pk': regie.pk})
108
    )
109
    assert delete_button.text() == 'Delete'
110

  
111

  
112
def test_manager_invoicing_regie_edit(app, admin_user):
113
    app = login(app)
114
    group_foo = Group.objects.create(name='role-foo')
115
    group_bar = Group.objects.create(name='role-bar')
116
    regie = Regie.objects.create(label='Foo', description='foo description', cashier_role=group_foo)
117
    resp = app.get(reverse('lingo-manager-invoicing-regie-edit', kwargs={'pk': regie.pk}))
118
    h2 = resp.pyquery('div#appbar h2')
119
    assert h2.text() == 'Edit regie - Foo'
120
    form = resp.form
121
    form.set('label', 'Foo bar')
122
    form.set('description', 'foo new description')
123
    form.set('cashier_role', group_bar.id)
124
    response = form.submit().follow()
125
    assert Regie.objects.count() == 1
126
    regie = Regie.objects.first()
127
    assert regie.label == 'Foo bar'
128
    assert regie.slug == 'foo'
129
    assert regie.description == 'foo new description'
130
    assert regie.cashier_role == group_bar
131
    assert urlparse(response.request.url).path == reverse(
132
        'lingo-manager-invoicing-regie-detail', kwargs={'pk': regie.pk}
133
    )
134

  
135

  
136
def test_manager_invoicing_regie_delete(app, admin_user):
137
    app = login(app)
138
    group = Group.objects.create(name='role-foo')
139
    regie = Regie.objects.create(label='Foo', description='foo description', cashier_role=group)
140
    assert Regie.objects.count() == 1
141
    resp = app.get(reverse('lingo-manager-invoicing-regie-delete', kwargs={'pk': regie.pk}))
142
    response = resp.form.submit().follow()
143
    assert Regie.objects.count() == 0
144
    assert urlparse(response.request.url).path == reverse('lingo-manager-invoicing-regie-list')
0
-