0001-invoicing-import-export-regies-69322.patch
lingo/invoicing/models.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import copy |
|
18 | ||
17 | 19 |
from django.contrib.auth.models import Group |
18 | 20 |
from django.db import models |
19 | 21 |
from django.utils.text import slugify |
... | ... | |
22 | 24 |
from lingo.utils.misc import generate_slug |
23 | 25 | |
24 | 26 | |
27 |
class RegieImportError(Exception): |
|
28 |
pass |
|
29 | ||
30 | ||
25 | 31 |
class Regie(models.Model): |
26 | 32 |
label = models.CharField(_('Label'), max_length=150) |
27 | 33 |
slug = models.SlugField(_('Identifier'), max_length=160, unique=True) |
... | ... | |
52 | 58 |
@property |
53 | 59 |
def base_slug(self): |
54 | 60 |
return slugify(self.label) |
61 | ||
62 |
def export_json(self): |
|
63 |
return { |
|
64 |
'label': self.label, |
|
65 |
'slug': self.slug, |
|
66 |
'description': self.description, |
|
67 |
'permissions': { |
|
68 |
'cashier': self.cashier_role.name if self.cashier_role else None, |
|
69 |
}, |
|
70 |
} |
|
71 | ||
72 |
@classmethod |
|
73 |
def import_json(cls, data): |
|
74 |
data = copy.deepcopy(data) |
|
75 |
permissions = data.pop('permissions') or {} |
|
76 |
role_name = permissions.get('cashier') |
|
77 |
if role_name: |
|
78 |
try: |
|
79 |
data['cashier_role'] = Group.objects.get(name=role_name) |
|
80 |
except Group.DoesNotExists: |
|
81 |
raise RegieImportError('Missing role: %s' % role_name) |
|
82 |
except Group.MultipleObjectsReturned: |
|
83 |
raise RegieImportError('Multiple role exist with the name: %s' % role_name) |
|
84 | ||
85 |
regie, created = cls.objects.update_or_create(slug=data['slug'], defaults=data) |
|
86 |
return created, regie |
lingo/invoicing/templates/lingo/invoicing/manager_import.html | ||
---|---|---|
1 |
{% extends "lingo/invoicing/manager_regie_list.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block breadcrumb %} |
|
5 |
{{ block.super }} |
|
6 |
<a href="{% url 'lingo-manager-invoicing-regie-import' %}">{% trans 'Import' %}</a> |
|
7 |
{% endblock %} |
|
8 | ||
9 |
{% block appbar %} |
|
10 |
<h2>{% trans "Import Regies" %}</h2> |
|
11 |
{% endblock %} |
|
12 | ||
13 |
{% block content %} |
|
14 |
<form method="post" enctype="multipart/form-data"> |
|
15 |
{% csrf_token %} |
|
16 |
{{ form.as_p }} |
|
17 |
<div class="buttons"> |
|
18 |
<button class="submit-button">{% trans "Import" %}</button> |
|
19 |
<a class="cancel" href="{% url 'lingo-manager-invoicing-regie-list' %}">{% trans 'Cancel' %}</a> |
|
20 |
</div> |
|
21 |
</form> |
|
22 |
{% endblock %} |
lingo/invoicing/templates/lingo/invoicing/manager_regie_list.html | ||
---|---|---|
4 | 4 |
{% block appbar %} |
5 | 5 |
<h2>{% trans 'Regies' %}</h2> |
6 | 6 |
<span class="actions"> |
7 |
<a class="extra-actions-menu-opener"></a> |
|
8 |
<ul class="extra-actions-menu"> |
|
9 |
<li> |
|
10 |
<a href="{% url 'lingo-manager-invoicing-regie-import' %}">{% trans 'Import' %}</a> |
|
11 |
</li> |
|
12 |
<li> |
|
13 |
<a href="{% url 'lingo-manager-invoicing-regie-export' %}">{% trans 'Export' %}</a> |
|
14 |
</li> |
|
15 |
</ul> |
|
7 | 16 |
<a rel="popup" href="{% url 'lingo-manager-invoicing-regie-add' %}">{% trans 'New regie' %}</a> |
8 | 17 |
</span> |
9 | 18 |
{% endblock %} |
lingo/invoicing/urls.py | ||
---|---|---|
41 | 41 |
views.regie_delete, |
42 | 42 |
name='lingo-manager-invoicing-regie-delete', |
43 | 43 |
), |
44 |
path('regies/import/', views.regies_import, name='lingo-manager-invoicing-regie-import'), |
|
45 |
path('regies/export/', views.regies_export, name='lingo-manager-invoicing-regie-export'), |
|
44 | 46 |
] |
lingo/invoicing/views.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 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 |
|
17 |
import collections |
|
18 |
import datetime |
|
19 |
import json |
|
20 | ||
21 |
from django.contrib import messages |
|
22 |
from django.db import transaction |
|
23 |
from django.http import HttpResponse |
|
24 |
from django.urls import reverse, reverse_lazy |
|
25 |
from django.utils.translation import ugettext_lazy as _ |
|
26 |
from django.utils.translation import ungettext |
|
27 |
from django.views.generic import ( |
|
28 |
CreateView, |
|
29 |
DeleteView, |
|
30 |
DetailView, |
|
31 |
FormView, |
|
32 |
ListView, |
|
33 |
TemplateView, |
|
34 |
UpdateView, |
|
35 |
) |
|
36 | ||
37 |
from lingo.invoicing.models import Regie, RegieImportError |
|
38 |
from lingo.pricing.forms import ImportForm |
|
39 | ||
40 | ||
41 |
def import_regies(data): |
|
42 |
results = collections.defaultdict(list) |
|
43 |
with transaction.atomic(): |
|
44 |
regies = data.get('regies', []) |
|
45 |
for regie in regies: |
|
46 |
created, regie_obj = Regie.import_json(regie) |
|
47 |
if created: |
|
48 |
results['created'].append(regie_obj) |
|
49 |
else: |
|
50 |
results['updated'].append(regie_obj) |
|
51 |
return results |
|
21 | 52 | |
22 | 53 | |
23 | 54 |
class HomeView(TemplateView): |
... | ... | |
81 | 112 | |
82 | 113 | |
83 | 114 |
regie_delete = RegieDeleteView.as_view() |
115 | ||
116 | ||
117 |
class RegiesExportView(ListView): |
|
118 |
model = Regie |
|
119 | ||
120 |
def get(self, request, *args, **kwargs): |
|
121 |
response = HttpResponse(content_type='application/json') |
|
122 |
today = datetime.date.today() |
|
123 |
attachment = 'attachment; filename="export_regies_{}.json"'.format(today.strftime('%Y%m%d')) |
|
124 |
response['Content-Disposition'] = attachment |
|
125 |
json.dump({'regies': [regie.export_json() for regie in self.get_queryset()]}, response, indent=2) |
|
126 |
return response |
|
127 | ||
128 | ||
129 |
regies_export = RegiesExportView.as_view() |
|
130 | ||
131 | ||
132 |
class RegiesImportView(FormView): |
|
133 |
form_class = ImportForm |
|
134 |
template_name = 'lingo/invoicing/manager_import.html' |
|
135 |
success_url = reverse_lazy('lingo-manager-invoicing-regie-list') |
|
136 | ||
137 |
def form_valid(self, form): |
|
138 |
try: |
|
139 |
config_json = json.loads(self.request.FILES['config_json'].read()) |
|
140 |
except ValueError: |
|
141 |
form.add_error('config_json', _('File is not in the expected JSON format.')) |
|
142 |
return self.form_invalid(form) |
|
143 | ||
144 |
try: |
|
145 |
results = import_regies(config_json) |
|
146 |
except RegieImportError as exc: |
|
147 |
form.add_error('config_json', '%s' % exc) |
|
148 |
return self.form_invalid(form) |
|
149 | ||
150 |
import_messages = { |
|
151 |
'create': lambda x: ungettext( |
|
152 |
'A regie was created.', |
|
153 |
'%(count)d regies were created.', |
|
154 |
x, |
|
155 |
), |
|
156 |
'update': lambda x: ungettext( |
|
157 |
'A regie was updated.', |
|
158 |
'%(count)d regie were updated.', |
|
159 |
x, |
|
160 |
), |
|
161 |
} |
|
162 |
create_message = _('No regie created.') |
|
163 |
update_message = _('No regie updated.') |
|
164 |
created = len(results.get('created', [])) |
|
165 |
updated = len(results.get('updated', [])) |
|
166 |
if created: |
|
167 |
create_message = import_messages.get('create')(created) % {'count': created} |
|
168 |
if updated: |
|
169 |
update_message = import_messages.get('update')(updated) % {'count': updated} |
|
170 |
message = "%s %s" % (create_message, update_message) |
|
171 |
messages.info(self.request, message) |
|
172 | ||
173 |
return super().form_valid(form) |
|
174 | ||
175 | ||
176 |
regies_import = RegiesImportView.as_view() |
tests/invoicing/test_manager.py | ||
---|---|---|
1 |
import json |
|
1 | 2 |
from urllib.parse import urlparse |
2 | 3 | |
3 | 4 |
import pytest |
4 | 5 |
from django.contrib.auth.models import Group |
5 | 6 |
from django.urls import reverse |
7 |
from webtest import Upload |
|
6 | 8 | |
7 | 9 |
from lingo.invoicing.models import Regie |
8 | 10 |
from tests.utils import login |
... | ... | |
142 | 144 |
response = resp.form.submit().follow() |
143 | 145 |
assert Regie.objects.count() == 0 |
144 | 146 |
assert urlparse(response.request.url).path == reverse('lingo-manager-invoicing-regie-list') |
147 | ||
148 | ||
149 |
def test_manager_invoicing_regie_import_export(app, admin_user, freezer): |
|
150 |
freezer.move_to('2020-06-15') |
|
151 |
app = login(app) |
|
152 |
group = Group.objects.create(name='role-foo') |
|
153 |
regie1 = Regie.objects.create(label='Foo', description='foo description', cashier_role=group) |
|
154 |
regie2 = Regie.objects.create(label='Bar', description='bar description', cashier_role=group) |
|
155 |
response = app.get(reverse('lingo-manager-invoicing-regie-export')) |
|
156 |
assert response.headers['content-type'] == 'application/json' |
|
157 |
assert response.headers['content-disposition'] == 'attachment; filename="export_regies_20200615.json"' |
|
158 |
regies_export = response.text |
|
159 |
regies_json = json.loads(regies_export) |
|
160 |
assert len(regies_json['regies']) == 2 |
|
161 | ||
162 |
regie1.delete() |
|
163 |
assert Regie.objects.count() == 1 |
|
164 |
response = app.get(reverse('lingo-manager-invoicing-regie-import')) |
|
165 |
response.form['config_json'] = Upload('export.json', regies_export.encode('utf-8'), 'application/json') |
|
166 |
response = response.form.submit().follow() |
|
167 |
assert urlparse(response.request.url).path == reverse('lingo-manager-invoicing-regie-list') |
|
168 |
assert 'A regie was created. A regie was updated.' in response.text |
|
169 |
assert Regie.objects.count() == 2 |
|
145 |
- |