diff --git a/lingo/invoicing/forms.py b/lingo/invoicing/forms.py
index f0db4e5..bd43fdf 100644
--- a/lingo/invoicing/forms.py
+++ b/lingo/invoicing/forms.py
@@ -23,6 +23,14 @@ from gadjo.forms.widgets import MultiSelectWidget
from lingo.invoicing.models import Campaign, DraftInvoice, DraftInvoiceLine, Invoice, InvoiceLine, Regie
+class ExportForm(forms.Form):
+ regies = forms.BooleanField(label=_('Regies'), required=False, initial=True)
+
+
+class ImportForm(forms.Form):
+ config_json = forms.FileField(label=_('Export File'))
+
+
class RegieForm(forms.ModelForm):
class Meta:
model = Regie
diff --git a/lingo/invoicing/templates/lingo/invoicing/export.html b/lingo/invoicing/templates/lingo/invoicing/export.html
new file mode 100644
index 0000000..9605ac9
--- /dev/null
+++ b/lingo/invoicing/templates/lingo/invoicing/export.html
@@ -0,0 +1,22 @@
+{% extends "lingo/invoicing/manager_home.html" %}
+{% load i18n %}
+
+{% block breadcrumb %}
+ {{ block.super }}
+ {% trans 'Export' %}
+{% endblock %}
+
+{% block appbar %}
+
{% trans "Export" %}
+{% endblock %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/lingo/invoicing/templates/lingo/invoicing/manager_import.html b/lingo/invoicing/templates/lingo/invoicing/import.html
similarity index 55%
rename from lingo/invoicing/templates/lingo/invoicing/manager_import.html
rename to lingo/invoicing/templates/lingo/invoicing/import.html
index 80e7920..69a6672 100644
--- a/lingo/invoicing/templates/lingo/invoicing/manager_import.html
+++ b/lingo/invoicing/templates/lingo/invoicing/import.html
@@ -1,13 +1,13 @@
-{% extends "lingo/invoicing/manager_regie_list.html" %}
+{% extends "lingo/invoicing/manager_home.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
- {% trans 'Import' %}
+ {% trans 'Import' %}
{% endblock %}
{% block appbar %}
- {% trans "Import Regies" %}
+ {% trans "Import" %}
{% endblock %}
{% block content %}
@@ -16,7 +16,7 @@
{{ form.as_p }}
{% endblock %}
diff --git a/lingo/invoicing/templates/lingo/invoicing/manager_home.html b/lingo/invoicing/templates/lingo/invoicing/manager_home.html
index b518c5f..8f8b143 100644
--- a/lingo/invoicing/templates/lingo/invoicing/manager_home.html
+++ b/lingo/invoicing/templates/lingo/invoicing/manager_home.html
@@ -8,6 +8,13 @@
{% block appbar %}
{% trans 'Invoicing' %}
+
+
+
+
{% endblock %}
{% block content %}
diff --git a/lingo/invoicing/templates/lingo/invoicing/manager_regie_detail.html b/lingo/invoicing/templates/lingo/invoicing/manager_regie_detail.html
index cb1dffc..1f9ab94 100644
--- a/lingo/invoicing/templates/lingo/invoicing/manager_regie_detail.html
+++ b/lingo/invoicing/templates/lingo/invoicing/manager_regie_detail.html
@@ -9,10 +9,14 @@
{% block appbar %}
{% trans 'Regie' %} - {{ regie }}
- {% if not has_related_objects %}
- {% trans "Delete" %}
- {% endif %}
- {% trans "Edit" %}
+
+
{% trans 'Non invoiced lines' %}
{% trans 'Invoices' %}
diff --git a/lingo/invoicing/templates/lingo/invoicing/manager_regie_list.html b/lingo/invoicing/templates/lingo/invoicing/manager_regie_list.html
index 24ee278..e4e47bf 100644
--- a/lingo/invoicing/templates/lingo/invoicing/manager_regie_list.html
+++ b/lingo/invoicing/templates/lingo/invoicing/manager_regie_list.html
@@ -9,15 +9,6 @@
{% block appbar %}
{% trans 'Regies' %}
-
-
{% trans 'New regie' %}
{% endblock %}
diff --git a/lingo/invoicing/urls.py b/lingo/invoicing/urls.py
index af0f8d9..3c64a8a 100644
--- a/lingo/invoicing/urls.py
+++ b/lingo/invoicing/urls.py
@@ -23,6 +23,8 @@ from .views import regie as regie_views
urlpatterns = [
path('', home_views.home, name='lingo-manager-invoicing-home'),
+ 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(
'regie/add/',
@@ -44,8 +46,11 @@ urlpatterns = [
regie_views.regie_delete,
name='lingo-manager-invoicing-regie-delete',
),
- path('regies/import/', regie_views.regies_import, name='lingo-manager-invoicing-regie-import'),
- path('regies/export/', regie_views.regies_export, name='lingo-manager-invoicing-regie-export'),
+ path(
+ 'regie//export/',
+ regie_views.regie_export,
+ name='lingo-manager-invoicing-regie-export',
+ ),
path(
'regie//campaign/add/',
campaign_views.campaign_add,
diff --git a/lingo/invoicing/utils.py b/lingo/invoicing/utils.py
index 5d259c5..5e27003 100644
--- a/lingo/invoicing/utils.py
+++ b/lingo/invoicing/utils.py
@@ -17,12 +17,13 @@
import collections
import datetime
+from django.db import transaction
from django.test.client import RequestFactory
from django.utils.translation import gettext_lazy as _
from lingo.agendas.chrono import get_check_status, get_subscriptions
from lingo.agendas.models import Agenda
-from lingo.invoicing.models import DraftInvoice, DraftInvoiceLine, InjectedLine
+from lingo.invoicing.models import DraftInvoice, DraftInvoiceLine, InjectedLine, Regie
from lingo.pricing.models import AgendaPricing, AgendaPricingNotFound, PricingError
@@ -310,3 +311,40 @@ def generate_invoices_from_lines(all_lines, pool):
invoices.append(invoice)
return invoices
+
+
+def export_site(
+ regies=True,
+):
+ '''Dump site objects to JSON-dumpable dictionnary'''
+ data = collections.OrderedDict()
+ if regies:
+ data['regies'] = [x.export_json() for x in Regie.objects.all()]
+ return data
+
+
+def import_site(data, if_empty=False, clean=False):
+ if if_empty and (Regie.objects.exists()):
+ return
+
+ if clean:
+ Regie.objects.all().delete()
+
+ results = {
+ key: collections.defaultdict(list)
+ for key in [
+ 'regies',
+ ]
+ }
+
+ with transaction.atomic():
+ for cls, key in ((Regie, 'regies'),):
+ objs = data.get(key, [])
+ for obj in objs:
+ created, obj = cls.import_json(obj)
+ results[key]['all'].append(obj)
+ if created:
+ results[key]['created'].append(obj)
+ else:
+ results[key]['updated'].append(obj)
+ return results
diff --git a/lingo/invoicing/views/home.py b/lingo/invoicing/views/home.py
index 612d578..a24cc81 100644
--- a/lingo/invoicing/views/home.py
+++ b/lingo/invoicing/views/home.py
@@ -14,7 +14,20 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.views.generic import TemplateView
+import datetime
+import json
+
+from django.contrib import messages
+from django.http import HttpResponse, HttpResponseRedirect
+from django.urls import reverse, reverse_lazy
+from django.utils.encoding import force_str
+from django.utils.translation import gettext_lazy as _
+from django.utils.translation import ngettext
+from django.views.generic import FormView, TemplateView
+
+from lingo.invoicing.forms import ExportForm, ImportForm
+from lingo.invoicing.utils import export_site, import_site
+from lingo.utils.misc import LingoImportError
class HomeView(TemplateView):
@@ -22,3 +35,100 @@ class HomeView(TemplateView):
home = HomeView.as_view()
+
+
+class ConfigExportView(FormView):
+ form_class = ExportForm
+ template_name = 'lingo/invoicing/export.html'
+
+ def form_valid(self, form):
+ response = HttpResponse(content_type='application/json')
+ today = datetime.date.today()
+ response['Content-Disposition'] = 'attachment; filename="export_invoicing_config_{}.json"'.format(
+ today.strftime('%Y%m%d')
+ )
+ json.dump(export_site(**form.cleaned_data), response, indent=2)
+ return response
+
+
+config_export = ConfigExportView.as_view()
+
+
+class ConfigImportView(FormView):
+ form_class = ImportForm
+ template_name = 'lingo/invoicing/import.html'
+ success_url = reverse_lazy('lingo-manager-invoicing-home')
+
+ def form_valid(self, form):
+ try:
+ config_json = json.loads(force_str(self.request.FILES['config_json'].read()))
+ except ValueError:
+ form.add_error('config_json', _('File is not in the expected JSON format.'))
+ return self.form_invalid(form)
+
+ try:
+ results = import_site(config_json)
+ except LingoImportError as exc:
+ form.add_error('config_json', '%s' % exc)
+ return self.form_invalid(form)
+ except KeyError as exc:
+ form.add_error('config_json', _('Key "%s" is missing.') % exc.args[0])
+ return self.form_invalid(form)
+
+ import_messages = {
+ 'regies': {
+ 'create_noop': _('No regie created.'),
+ 'create': lambda x: ngettext(
+ 'A regie has been created.',
+ '%(count)d regies have been created.',
+ x,
+ ),
+ 'update_noop': _('No regie updated.'),
+ 'update': lambda x: ngettext(
+ 'A regie has been updated.',
+ '%(count)d regies have been updated.',
+ x,
+ ),
+ },
+ }
+
+ global_noop = True
+ for obj_name, obj_results in results.items():
+ if obj_results['all']:
+ global_noop = False
+ count = len(obj_results['created'])
+ if not count:
+ message1 = import_messages[obj_name].get('create_noop')
+ else:
+ message1 = import_messages[obj_name]['create'](count) % {'count': count}
+
+ count = len(obj_results['updated'])
+ if not count:
+ message2 = import_messages[obj_name]['update_noop']
+ else:
+ message2 = import_messages[obj_name]['update'](count) % {'count': count}
+
+ if message1:
+ obj_results['messages'] = "%s %s" % (message1, message2)
+ else:
+ obj_results['messages'] = message2
+
+ (r_count,) = (len(results['regies']['all']),)
+ if (r_count,) == (1,):
+ # only one regie imported, redirect to regie page
+ return HttpResponseRedirect(
+ reverse(
+ 'lingo-manager-invoicing-regie-detail',
+ kwargs={'pk': results['regies']['all'][0].pk},
+ )
+ )
+
+ if global_noop:
+ messages.info(self.request, _('No data found.'))
+ else:
+ messages.info(self.request, results['regies']['messages'])
+
+ return super().form_valid(form)
+
+
+config_import = ConfigImportView.as_view()
diff --git a/lingo/invoicing/views/regie.py b/lingo/invoicing/views/regie.py
index 5d32083..76a5a1c 100644
--- a/lingo/invoicing/views/regie.py
+++ b/lingo/invoicing/views/regie.py
@@ -18,30 +18,17 @@ import collections
import datetime
import json
-from django.contrib import messages
from django.db import transaction
from django.db.models import CharField, IntegerField, JSONField, Value
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
-from django.urls import reverse, reverse_lazy
-from django.utils.translation import gettext_lazy as _
-from django.utils.translation import ngettext
-from django.views.generic import CreateView, DeleteView, DetailView, FormView, ListView, UpdateView
+from django.urls import reverse
+from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView
from lingo.agendas.models import Agenda
from lingo.invoicing.forms import RegieForm, RegieInvoiceFilterSet
-from lingo.invoicing.models import (
- Counter,
- InjectedLine,
- Invoice,
- InvoiceLine,
- InvoicePayment,
- Pool,
- Regie,
- RegieImportError,
-)
+from lingo.invoicing.models import Counter, InjectedLine, Invoice, InvoiceLine, InvoicePayment, Pool, Regie
from lingo.invoicing.views.utils import PDFMixin
-from lingo.pricing.forms import ImportForm
def import_regies(data):
@@ -128,66 +115,21 @@ class RegieDeleteView(DeleteView):
regie_delete = RegieDeleteView.as_view()
-class RegiesExportView(ListView):
+class RegieExport(DetailView):
model = Regie
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type='application/json')
today = datetime.date.today()
- attachment = 'attachment; filename="export_regies_{}.json"'.format(today.strftime('%Y%m%d'))
+ attachment = 'attachment; filename="export_regie_{}_{}.json"'.format(
+ self.get_object().slug, today.strftime('%Y%m%d')
+ )
response['Content-Disposition'] = attachment
- json.dump({'regies': [regie.export_json() for regie in self.get_queryset()]}, response, indent=2)
+ json.dump({'regies': [self.get_object().export_json()]}, response, indent=2)
return response
-regies_export = RegiesExportView.as_view()
-
-
-class RegiesImportView(FormView):
- form_class = ImportForm
- template_name = 'lingo/invoicing/manager_import.html'
- success_url = reverse_lazy('lingo-manager-invoicing-regie-list')
-
- def form_valid(self, form):
- try:
- config_json = json.loads(self.request.FILES['config_json'].read())
- except ValueError:
- form.add_error('config_json', _('File is not in the expected JSON format.'))
- return self.form_invalid(form)
-
- try:
- results = import_regies(config_json)
- except RegieImportError as exc:
- form.add_error('config_json', '%s' % exc)
- return self.form_invalid(form)
-
- import_messages = {
- 'create': lambda x: ngettext(
- 'A regie was created.',
- '%(count)d regies were created.',
- x,
- ),
- 'update': lambda x: ngettext(
- 'A regie was updated.',
- '%(count)d regie were updated.',
- x,
- ),
- }
- create_message = _('No regie created.')
- update_message = _('No regie updated.')
- created = len(results.get('created', []))
- updated = len(results.get('updated', []))
- if created:
- create_message = import_messages.get('create')(created) % {'count': created}
- if updated:
- update_message = import_messages.get('update')(updated) % {'count': updated}
- message = "%s %s" % (create_message, update_message)
- messages.info(self.request, message)
-
- return super().form_valid(form)
-
-
-regies_import = RegiesImportView.as_view()
+regie_export = RegieExport.as_view()
class NonInvoicedLineListView(ListView):
diff --git a/lingo/pricing/templates/lingo/pricing/export.html b/lingo/pricing/templates/lingo/pricing/export.html
index 9531773..8b9323d 100644
--- a/lingo/pricing/templates/lingo/pricing/export.html
+++ b/lingo/pricing/templates/lingo/pricing/export.html
@@ -1,4 +1,4 @@
-{% extends "lingo/pricing/manager_pricing_list.html" %}
+{% extends "lingo/pricing/manager_home.html" %}
{% load i18n %}
{% block breadcrumb %}
@@ -16,7 +16,7 @@
{{ form.as_p }}
{% endblock %}
diff --git a/lingo/pricing/templates/lingo/pricing/import.html b/lingo/pricing/templates/lingo/pricing/import.html
index 96d754e..3b8771f 100644
--- a/lingo/pricing/templates/lingo/pricing/import.html
+++ b/lingo/pricing/templates/lingo/pricing/import.html
@@ -1,4 +1,4 @@
-{% extends "lingo/pricing/manager_pricing_list.html" %}
+{% extends "lingo/pricing/manager_home.html" %}
{% load i18n %}
{% block breadcrumb %}
@@ -16,7 +16,7 @@
{{ form.as_p }}
{% endblock %}
diff --git a/tests/invoicing/manager/test_import_export.py b/tests/invoicing/manager/test_import_export.py
new file mode 100644
index 0000000..ce7e530
--- /dev/null
+++ b/tests/invoicing/manager/test_import_export.py
@@ -0,0 +1,95 @@
+import copy
+import json
+
+import pytest
+from webtest import Upload
+
+from lingo.invoicing.models import Regie
+from tests.utils import login
+
+pytestmark = pytest.mark.django_db
+
+
+def test_export_site(freezer, app, admin_user):
+ freezer.move_to('2020-06-15')
+ login(app)
+ resp = app.get('/manage/invoicing/')
+ resp = resp.click('Export')
+
+ resp = resp.form.submit()
+ assert resp.headers['content-type'] == 'application/json'
+ assert (
+ resp.headers['content-disposition'] == 'attachment; filename="export_invoicing_config_20200615.json"'
+ )
+
+ site_json = json.loads(resp.text)
+ assert site_json == {
+ 'regies': [],
+ }
+
+ Regie.objects.create(label='Foo Bar')
+ resp = app.get('/manage/invoicing/export/')
+ resp = resp.form.submit()
+
+ site_json = json.loads(resp.text)
+ assert len(site_json['regies']) == 1
+
+
+@pytest.mark.freeze_time('2023-06-02')
+def test_import_regie(app, admin_user):
+ regie = Regie.objects.create(label='Foo bar')
+
+ app = login(app)
+ resp = app.get('/manage/invoicing/regie/%s/' % regie.pk)
+ resp = resp.click(href='/manage/invoicing/regie/%s/export/' % regie.pk)
+ assert resp.headers['content-type'] == 'application/json'
+ assert resp.headers['content-disposition'] == 'attachment; filename="export_regie_foo-bar_20230602.json"'
+ regie_export = resp.text
+
+ # existing regie
+ resp = app.get('/manage/invoicing/', status=200)
+ resp = resp.click('Import')
+ resp.form['config_json'] = Upload('export.json', regie_export.encode('utf-8'), 'application/json')
+ resp = resp.form.submit()
+ assert resp.location.endswith('/manage/invoicing/regie/%s/' % regie.pk)
+ resp = resp.follow()
+ assert 'No regie created. A regie has been updated.' not in resp.text
+ assert Regie.objects.count() == 1
+
+ # new regie
+ Regie.objects.all().delete()
+ resp = app.get('/manage/invoicing/', status=200)
+ resp = resp.click('Import')
+ resp.form['config_json'] = Upload('export.json', regie_export.encode('utf-8'), 'application/json')
+ resp = resp.form.submit()
+ regie = Regie.objects.latest('pk')
+ assert resp.location.endswith('/manage/invoicing/regie/%s/' % regie.pk)
+ resp = resp.follow()
+ assert 'A regie has been created. No regie updated.' not in resp.text
+ assert Regie.objects.count() == 1
+
+ # multiple regies
+ regies = json.loads(regie_export)
+ regies['regies'].append(copy.copy(regies['regies'][0]))
+ regies['regies'].append(copy.copy(regies['regies'][0]))
+ regies['regies'][1]['label'] = 'Foo bar 2'
+ regies['regies'][1]['slug'] = 'foo-bar-2'
+ regies['regies'][2]['label'] = 'Foo bar 3'
+ regies['regies'][2]['slug'] = 'foo-bar-3'
+
+ resp = app.get('/manage/invoicing/', status=200)
+ resp = resp.click('Import')
+ resp.form['config_json'] = Upload('export.json', json.dumps(regies).encode('utf-8'), 'application/json')
+ resp = resp.form.submit()
+ assert resp.location.endswith('/manage/invoicing/')
+ resp = resp.follow()
+ assert '2 regies have been created. A regie has been updated.' in resp.text
+ assert Regie.objects.count() == 3
+
+ Regie.objects.all().delete()
+ resp = app.get('/manage/invoicing/', status=200)
+ resp = resp.click('Import')
+ resp.form['config_json'] = Upload('export.json', json.dumps(regies).encode('utf-8'), 'application/json')
+ resp = resp.form.submit().follow()
+ assert '3 regies have been created. No regie updated.' in resp.text
+ assert Regie.objects.count() == 3
diff --git a/tests/invoicing/manager/test_regie.py b/tests/invoicing/manager/test_regie.py
index 376d740..dd3b5a9 100644
--- a/tests/invoicing/manager/test_regie.py
+++ b/tests/invoicing/manager/test_regie.py
@@ -1,5 +1,4 @@
import datetime
-import json
from urllib.parse import urlparse
import pytest
@@ -7,7 +6,6 @@ from django.contrib.auth.models import Group
from django.urls import reverse
from django.utils.formats import date_format
from django.utils.timezone import localtime
-from webtest import Upload
from lingo.agendas.models import Agenda
from lingo.invoicing.models import (
@@ -225,29 +223,6 @@ def test_manager_invoicing_regie_delete(app, admin_user):
assert Counter.objects.count() == 0
-def test_manager_invoicing_regie_import_export(app, admin_user, freezer):
- freezer.move_to('2020-06-15')
- app = login(app)
- group = Group.objects.create(name='role-foo')
- regie1 = Regie.objects.create(label='Foo', description='foo description', cashier_role=group)
- Regie.objects.create(label='Bar', description='bar description', cashier_role=group)
- response = app.get(reverse('lingo-manager-invoicing-regie-export'))
- assert response.headers['content-type'] == 'application/json'
- assert response.headers['content-disposition'] == 'attachment; filename="export_regies_20200615.json"'
- regies_export = response.text
- regies_json = json.loads(regies_export)
- assert len(regies_json['regies']) == 2
-
- regie1.delete()
- assert Regie.objects.count() == 1
- response = app.get(reverse('lingo-manager-invoicing-regie-import'))
- response.form['config_json'] = Upload('export.json', regies_export.encode('utf-8'), 'application/json')
- response = response.form.submit().follow()
- assert urlparse(response.request.url).path == reverse('lingo-manager-invoicing-regie-list')
- assert 'A regie was created. A regie was updated.' in response.text
- assert Regie.objects.count() == 2
-
-
def test_non_invoiced_line_list(app, admin_user):
regie = Regie.objects.create(label='Regie')
other_regie = Regie.objects.create(label='Other Regie')