mirror of
https://gitlab.crans.org/bde/nk20
synced 2024-11-26 18:37:12 +00:00
Use custom inputs for date picker and amounts
This commit is contained in:
parent
45b14ed1bd
commit
f81e2b5b5b
@ -2,11 +2,15 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from activity.models import Activity
|
from activity.models import Activity
|
||||||
|
from note_kfet.inputs import DateTimePickerInput
|
||||||
|
|
||||||
|
|
||||||
class ActivityForm(forms.ModelForm):
|
class ActivityForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Activity
|
model = Activity
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
widgets = {
|
||||||
|
"date_start": DateTimePickerInput(),
|
||||||
|
"date_end": DateTimePickerInput(),
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
|
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_tables2.views import SingleTableView
|
from django_tables2.views import SingleTableView
|
||||||
@ -9,12 +10,12 @@ from .forms import ActivityForm
|
|||||||
from .models import Activity
|
from .models import Activity
|
||||||
|
|
||||||
|
|
||||||
class ActivityCreateView(CreateView):
|
class ActivityCreateView(LoginRequiredMixin, CreateView):
|
||||||
model = Activity
|
model = Activity
|
||||||
form_class = ActivityForm
|
form_class = ActivityForm
|
||||||
|
|
||||||
|
|
||||||
class ActivityListView(SingleTableView):
|
class ActivityListView(LoginRequiredMixin, SingleTableView):
|
||||||
model = Activity
|
model = Activity
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@ -25,14 +26,14 @@ class ActivityListView(SingleTableView):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class ActivityDetailView(DetailView):
|
class ActivityDetailView(LoginRequiredMixin, DetailView):
|
||||||
model = Activity
|
model = Activity
|
||||||
|
|
||||||
|
|
||||||
class ActivityUpdateView(UpdateView):
|
class ActivityUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
model = Activity
|
model = Activity
|
||||||
form_class = ActivityForm
|
form_class = ActivityForm
|
||||||
|
|
||||||
|
|
||||||
class ActivityEntryView(TemplateView):
|
class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||||
pass
|
pass
|
||||||
|
@ -18,10 +18,5 @@ def pretty_money(value):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def cents_to_euros(value):
|
|
||||||
return "{:.02f}".format(value / 100) if value else ""
|
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
register.filter('pretty_money', pretty_money)
|
register.filter('pretty_money', pretty_money)
|
||||||
register.filter('cents_to_euros', cents_to_euros)
|
|
||||||
|
@ -9,6 +9,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.views.generic import CreateView, UpdateView
|
from django.views.generic import CreateView, UpdateView
|
||||||
from django_tables2 import SingleTableView
|
from django_tables2 import SingleTableView
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
from note_kfet.inputs import AmountInput
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
from .forms import TransactionTemplateForm
|
from .forms import TransactionTemplateForm
|
||||||
@ -40,6 +41,7 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView):
|
|||||||
"""
|
"""
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['title'] = _('Transfer money')
|
context['title'] = _('Transfer money')
|
||||||
|
context['amount_widget'] = AmountInput(attrs={"id": "amount"})
|
||||||
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
|
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
|
||||||
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
|
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
|
||||||
context['special_types'] = NoteSpecial.objects.order_by("special_type").all()
|
context['special_types'] = NoteSpecial.objects.order_by("special_type").all()
|
||||||
|
@ -7,6 +7,7 @@ from crispy_forms.helper import FormHelper
|
|||||||
from crispy_forms.layout import Submit
|
from crispy_forms.layout import Submit
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from note_kfet.inputs import DatePickerInput, AmountInput
|
||||||
|
|
||||||
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
|
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
|
||||||
|
|
||||||
@ -19,7 +20,7 @@ class InvoiceForm(forms.ModelForm):
|
|||||||
# Django forms don't support date fields. We have to add it manually
|
# Django forms don't support date fields. We have to add it manually
|
||||||
date = forms.DateField(
|
date = forms.DateField(
|
||||||
initial=datetime.date.today,
|
initial=datetime.date.today,
|
||||||
widget=forms.TextInput(attrs={'type': 'date'})
|
widget=DatePickerInput()
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean_date(self):
|
def clean_date(self):
|
||||||
@ -30,12 +31,21 @@ class InvoiceForm(forms.ModelForm):
|
|||||||
exclude = ('bde', )
|
exclude = ('bde', )
|
||||||
|
|
||||||
|
|
||||||
|
class ProductForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Product
|
||||||
|
fields = '__all__'
|
||||||
|
widgets = {
|
||||||
|
"amount": AmountInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Add a subform per product in the invoice form, and manage correctly the link between the invoice and
|
# Add a subform per product in the invoice form, and manage correctly the link between the invoice and
|
||||||
# its products. The FormSet will search automatically the ForeignKey in the Product model.
|
# its products. The FormSet will search automatically the ForeignKey in the Product model.
|
||||||
ProductFormSet = forms.inlineformset_factory(
|
ProductFormSet = forms.inlineformset_factory(
|
||||||
Invoice,
|
Invoice,
|
||||||
Product,
|
Product,
|
||||||
fields='__all__',
|
form=ProductForm,
|
||||||
extra=1,
|
extra=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,18 +50,8 @@ class InvoiceCreateView(LoginRequiredMixin, CreateView):
|
|||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
ret = super().form_valid(form)
|
ret = super().form_valid(form)
|
||||||
|
|
||||||
kwargs = {}
|
|
||||||
|
|
||||||
# The user type amounts in cents. We convert it in euros.
|
|
||||||
for key in self.request.POST:
|
|
||||||
value = self.request.POST[key]
|
|
||||||
if key.endswith("amount") and value:
|
|
||||||
kwargs[key] = str(int(100 * float(value)))
|
|
||||||
elif value:
|
|
||||||
kwargs[key] = value
|
|
||||||
|
|
||||||
# For each product, we save it
|
# For each product, we save it
|
||||||
formset = ProductFormSet(kwargs, instance=form.instance)
|
formset = ProductFormSet(self.request.POST, instance=form.instance)
|
||||||
if formset.is_valid():
|
if formset.is_valid():
|
||||||
for f in formset:
|
for f in formset:
|
||||||
# We don't save the product if the designation is not entered, ie. if the line is empty
|
# We don't save the product if the designation is not entered, ie. if the line is empty
|
||||||
@ -112,16 +102,7 @@ class InvoiceUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
ret = super().form_valid(form)
|
ret = super().form_valid(form)
|
||||||
|
|
||||||
kwargs = {}
|
formset = ProductFormSet(self.request.POST, instance=form.instance)
|
||||||
# The user type amounts in cents. We convert it in euros.
|
|
||||||
for key in self.request.POST:
|
|
||||||
value = self.request.POST[key]
|
|
||||||
if key.endswith("amount") and value:
|
|
||||||
kwargs[key] = str(int(100 * float(value)))
|
|
||||||
elif value:
|
|
||||||
kwargs[key] = value
|
|
||||||
|
|
||||||
formset = ProductFormSet(kwargs, instance=form.instance)
|
|
||||||
saved = []
|
saved = []
|
||||||
# For each product, we save it
|
# For each product, we save it
|
||||||
if formset.is_valid():
|
if formset.is_valid():
|
||||||
|
280
note_kfet/inputs.py
Normal file
280
note_kfet/inputs.py
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
"""
|
||||||
|
This file comes from the project `django-bootstrap-datepicker-plus` available on Github:
|
||||||
|
https://github.com/monim67/django-bootstrap-datepicker-plus
|
||||||
|
This is distributed under Apache License 2.0.
|
||||||
|
|
||||||
|
This adds datetime pickers with bootstrap.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Contains Base Date-Picker input class for widgets of this package."""
|
||||||
|
|
||||||
|
from json import dumps as json_dumps
|
||||||
|
|
||||||
|
from django.forms.widgets import DateTimeBaseInput, NumberInput
|
||||||
|
|
||||||
|
|
||||||
|
class AmountInput(NumberInput):
|
||||||
|
"""
|
||||||
|
This input type lets the user type amounts in euros, but forms receive data in cents
|
||||||
|
"""
|
||||||
|
template_name = "note/amount_input.html"
|
||||||
|
|
||||||
|
def format_value(self, value):
|
||||||
|
return None if value is None or value == "" else "{:.02f}".format(value / 100, )
|
||||||
|
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
val = super().value_from_datadict(data, files, name)
|
||||||
|
return str(int(100 * float(val))) if val else val
|
||||||
|
|
||||||
|
|
||||||
|
class DatePickerDictionary:
|
||||||
|
"""Keeps track of all date-picker input classes."""
|
||||||
|
|
||||||
|
_i = 0
|
||||||
|
items = dict()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_id(cls):
|
||||||
|
"""Return a unique ID for each date-picker input class."""
|
||||||
|
cls._i += 1
|
||||||
|
return 'dp_%s' % cls._i
|
||||||
|
|
||||||
|
|
||||||
|
class BasePickerInput(DateTimeBaseInput):
|
||||||
|
"""Base Date-Picker input class for widgets of this package."""
|
||||||
|
|
||||||
|
template_name = 'bootstrap_datepicker_plus/date_picker.html'
|
||||||
|
picker_type = 'DATE'
|
||||||
|
format = '%Y-%m-%d'
|
||||||
|
config = {}
|
||||||
|
_default_config = {
|
||||||
|
'id': None,
|
||||||
|
'picker_type': None,
|
||||||
|
'linked_to': None,
|
||||||
|
'options': {} # final merged options
|
||||||
|
}
|
||||||
|
options = {} # options extended by user
|
||||||
|
options_param = {} # options passed as parameter
|
||||||
|
_default_options = {
|
||||||
|
'showClose': True,
|
||||||
|
'showClear': True,
|
||||||
|
'showTodayButton': True,
|
||||||
|
"locale": "fr",
|
||||||
|
}
|
||||||
|
|
||||||
|
# source: https://github.com/tutorcruncher/django-bootstrap3-datetimepicker
|
||||||
|
# file: /blob/31fbb09/bootstrap3_datetime/widgets.py#L33
|
||||||
|
format_map = (
|
||||||
|
('DDD', r'%j'),
|
||||||
|
('DD', r'%d'),
|
||||||
|
('MMMM', r'%B'),
|
||||||
|
('MMM', r'%b'),
|
||||||
|
('MM', r'%m'),
|
||||||
|
('YYYY', r'%Y'),
|
||||||
|
('YY', r'%y'),
|
||||||
|
('HH', r'%H'),
|
||||||
|
('hh', r'%I'),
|
||||||
|
('mm', r'%M'),
|
||||||
|
('ss', r'%S'),
|
||||||
|
('a', r'%p'),
|
||||||
|
('ZZ', r'%z'),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
"""JS/CSS resources needed to render the date-picker calendar."""
|
||||||
|
|
||||||
|
js = (
|
||||||
|
'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.9.0/'
|
||||||
|
'moment-with-locales.min.js',
|
||||||
|
'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/'
|
||||||
|
'4.17.47/js/bootstrap-datetimepicker.min.js',
|
||||||
|
'bootstrap_datepicker_plus/js/datepicker-widget.js'
|
||||||
|
)
|
||||||
|
css = {'all': (
|
||||||
|
'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/'
|
||||||
|
'4.17.47/css/bootstrap-datetimepicker.css',
|
||||||
|
'bootstrap_datepicker_plus/css/datepicker-widget.css'
|
||||||
|
), }
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def format_py2js(cls, datetime_format):
|
||||||
|
"""Convert python datetime format to moment datetime format."""
|
||||||
|
for js_format, py_format in cls.format_map:
|
||||||
|
datetime_format = datetime_format.replace(py_format, js_format)
|
||||||
|
return datetime_format
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def format_js2py(cls, datetime_format):
|
||||||
|
"""Convert moment datetime format to python datetime format."""
|
||||||
|
for js_format, py_format in cls.format_map:
|
||||||
|
datetime_format = datetime_format.replace(js_format, py_format)
|
||||||
|
return datetime_format
|
||||||
|
|
||||||
|
def __init__(self, attrs=None, format=None, options=None):
|
||||||
|
"""Initialize the Date-picker widget."""
|
||||||
|
self.format_param = format
|
||||||
|
self.options_param = options if options else {}
|
||||||
|
self.config = self._default_config.copy()
|
||||||
|
self.config['id'] = DatePickerDictionary.generate_id()
|
||||||
|
self.config['picker_type'] = self.picker_type
|
||||||
|
self.config['options'] = self._calculate_options()
|
||||||
|
attrs = attrs if attrs else {}
|
||||||
|
if 'class' not in attrs:
|
||||||
|
attrs['class'] = 'form-control'
|
||||||
|
super().__init__(attrs, self._calculate_format())
|
||||||
|
|
||||||
|
def _calculate_options(self):
|
||||||
|
"""Calculate and Return the options."""
|
||||||
|
_options = self._default_options.copy()
|
||||||
|
_options.update(self.options)
|
||||||
|
if self.options_param:
|
||||||
|
_options.update(self.options_param)
|
||||||
|
return _options
|
||||||
|
|
||||||
|
def _calculate_format(self):
|
||||||
|
"""Calculate and Return the datetime format."""
|
||||||
|
_format = self.format_param if self.format_param else self.format
|
||||||
|
if self.config['options'].get('format'):
|
||||||
|
_format = self.format_js2py(self.config['options'].get('format'))
|
||||||
|
else:
|
||||||
|
self.config['options']['format'] = self.format_py2js(_format)
|
||||||
|
return _format
|
||||||
|
|
||||||
|
def get_context(self, name, value, attrs):
|
||||||
|
"""Return widget context dictionary."""
|
||||||
|
context = super().get_context(
|
||||||
|
name, value, attrs)
|
||||||
|
context['widget']['attrs']['dp_config'] = json_dumps(self.config)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def start_of(self, event_id):
|
||||||
|
"""
|
||||||
|
Set Date-Picker as the start-date of a date-range.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- event_id (string): User-defined unique id for linking two fields
|
||||||
|
"""
|
||||||
|
DatePickerDictionary.items[str(event_id)] = self
|
||||||
|
return self
|
||||||
|
|
||||||
|
def end_of(self, event_id, import_options=True):
|
||||||
|
"""
|
||||||
|
Set Date-Picker as the end-date of a date-range.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- event_id (string): User-defined unique id for linking two fields
|
||||||
|
- import_options (bool): inherit options from start-date input,
|
||||||
|
default: TRUE
|
||||||
|
"""
|
||||||
|
event_id = str(event_id)
|
||||||
|
if event_id in DatePickerDictionary.items:
|
||||||
|
linked_picker = DatePickerDictionary.items[event_id]
|
||||||
|
self.config['linked_to'] = linked_picker.config['id']
|
||||||
|
if import_options:
|
||||||
|
backup_moment_format = self.config['options']['format']
|
||||||
|
self.config['options'].update(linked_picker.config['options'])
|
||||||
|
self.config['options'].update(self.options_param)
|
||||||
|
if self.format_param or 'format' in self.options_param:
|
||||||
|
self.config['options']['format'] = backup_moment_format
|
||||||
|
else:
|
||||||
|
self.format = linked_picker.format
|
||||||
|
# Setting useCurrent is necessary, see following issue
|
||||||
|
# https://github.com/Eonasdan/bootstrap-datetimepicker/issues/1075
|
||||||
|
self.config['options']['useCurrent'] = False
|
||||||
|
self._link_to(linked_picker)
|
||||||
|
else:
|
||||||
|
raise KeyError(
|
||||||
|
'start-date not specified for event_id "%s"' % event_id)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _link_to(self, linked_picker):
|
||||||
|
"""
|
||||||
|
Executed when two date-inputs are linked together.
|
||||||
|
|
||||||
|
This method for sub-classes to override to customize the linking.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DatePickerInput(BasePickerInput):
|
||||||
|
"""
|
||||||
|
Widget to display a Date-Picker Calendar on a DateField property.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- attrs (dict): HTML attributes of rendered HTML input
|
||||||
|
- format (string): Python DateTime format eg. "%Y-%m-%d"
|
||||||
|
- options (dict): Options to customize the widget, see README
|
||||||
|
"""
|
||||||
|
|
||||||
|
picker_type = 'DATE'
|
||||||
|
format = '%Y-%m-%d'
|
||||||
|
format_key = 'DATE_INPUT_FORMATS'
|
||||||
|
|
||||||
|
|
||||||
|
class TimePickerInput(BasePickerInput):
|
||||||
|
"""
|
||||||
|
Widget to display a Time-Picker Calendar on a TimeField property.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- attrs (dict): HTML attributes of rendered HTML input
|
||||||
|
- format (string): Python DateTime format eg. "%Y-%m-%d"
|
||||||
|
- options (dict): Options to customize the widget, see README
|
||||||
|
"""
|
||||||
|
|
||||||
|
picker_type = 'TIME'
|
||||||
|
format = '%H:%M'
|
||||||
|
format_key = 'TIME_INPUT_FORMATS'
|
||||||
|
template_name = 'bootstrap_datepicker_plus/time_picker.html'
|
||||||
|
|
||||||
|
|
||||||
|
class DateTimePickerInput(BasePickerInput):
|
||||||
|
"""
|
||||||
|
Widget to display a DateTime-Picker Calendar on a DateTimeField property.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- attrs (dict): HTML attributes of rendered HTML input
|
||||||
|
- format (string): Python DateTime format eg. "%Y-%m-%d"
|
||||||
|
- options (dict): Options to customize the widget, see README
|
||||||
|
"""
|
||||||
|
|
||||||
|
picker_type = 'DATETIME'
|
||||||
|
format = '%Y-%m-%d %H:%M'
|
||||||
|
format_key = 'DATETIME_INPUT_FORMATS'
|
||||||
|
|
||||||
|
|
||||||
|
class MonthPickerInput(BasePickerInput):
|
||||||
|
"""
|
||||||
|
Widget to display a Month-Picker Calendar on a DateField property.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- attrs (dict): HTML attributes of rendered HTML input
|
||||||
|
- format (string): Python DateTime format eg. "%Y-%m-%d"
|
||||||
|
- options (dict): Options to customize the widget, see README
|
||||||
|
"""
|
||||||
|
|
||||||
|
picker_type = 'MONTH'
|
||||||
|
format = '01/%m/%Y'
|
||||||
|
format_key = 'DATE_INPUT_FORMATS'
|
||||||
|
|
||||||
|
|
||||||
|
class YearPickerInput(BasePickerInput):
|
||||||
|
"""
|
||||||
|
Widget to display a Year-Picker Calendar on a DateField property.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
- attrs (dict): HTML attributes of rendered HTML input
|
||||||
|
- format (string): Python DateTime format eg. "%Y-%m-%d"
|
||||||
|
- options (dict): Options to customize the widget, see README
|
||||||
|
"""
|
||||||
|
|
||||||
|
picker_type = 'YEAR'
|
||||||
|
format = '01/01/%Y'
|
||||||
|
format_key = 'DATE_INPUT_FORMATS'
|
||||||
|
|
||||||
|
def _link_to(self, linked_picker):
|
||||||
|
"""Customize the options when linked with other date-time input"""
|
||||||
|
yformat = self.config['options']['format'].replace('-01-01', '-12-31')
|
||||||
|
self.config['options']['format'] = yformat
|
@ -48,6 +48,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sites',
|
'django.contrib.sites',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
'django.forms',
|
||||||
# API
|
# API
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework.authtoken',
|
'rest_framework.authtoken',
|
||||||
@ -100,6 +101,8 @@ TEMPLATES = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
|
||||||
|
|
||||||
WSGI_APPLICATION = 'note_kfet.wsgi.application'
|
WSGI_APPLICATION = 'note_kfet.wsgi.application'
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
|
121
static/bootstrap_datepicker_plus/css/datepicker-widget.css
Normal file
121
static/bootstrap_datepicker_plus/css/datepicker-widget.css
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Glyphicons Halflings';
|
||||||
|
src: url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot');
|
||||||
|
src: url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
|
||||||
|
url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2') format('woff2'),
|
||||||
|
url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff') format('woff'),
|
||||||
|
url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.ttf') format('truetype'),
|
||||||
|
url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.glyphicon {
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
display: inline-block;
|
||||||
|
font-family: 'Glyphicons Halflings';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 1;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glyphicon-time:before {
|
||||||
|
content: "\e023";
|
||||||
|
}
|
||||||
|
|
||||||
|
.glyphicon-chevron-left:before {
|
||||||
|
content: "\e079";
|
||||||
|
}
|
||||||
|
|
||||||
|
.glyphicon-chevron-right:before {
|
||||||
|
content: "\e080";
|
||||||
|
}
|
||||||
|
|
||||||
|
.glyphicon-chevron-up:before {
|
||||||
|
content: "\e113";
|
||||||
|
}
|
||||||
|
|
||||||
|
.glyphicon-chevron-down:before {
|
||||||
|
content: "\e114";
|
||||||
|
}
|
||||||
|
|
||||||
|
.glyphicon-calendar:before {
|
||||||
|
content: "\e109";
|
||||||
|
}
|
||||||
|
|
||||||
|
.glyphicon-screenshot:before {
|
||||||
|
content: "\e087";
|
||||||
|
}
|
||||||
|
|
||||||
|
.glyphicon-trash:before {
|
||||||
|
content: "\e020";
|
||||||
|
}
|
||||||
|
|
||||||
|
.glyphicon-remove:before {
|
||||||
|
content: "\e014";
|
||||||
|
}
|
||||||
|
|
||||||
|
.bootstrap-datetimepicker-widget .btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6px 12px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 1.42857143;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
-ms-touch-action: manipulation;
|
||||||
|
touch-action: manipulation;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
background-image: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bootstrap-datetimepicker-widget.dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
display: none;
|
||||||
|
float: left;
|
||||||
|
min-width: 160px;
|
||||||
|
padding: 5px 0;
|
||||||
|
margin: 2px 0 0;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: left;
|
||||||
|
list-style: none;
|
||||||
|
background-color: #fff;
|
||||||
|
-webkit-background-clip: padding-box;
|
||||||
|
background-clip: padding-box;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border: 1px solid rgba(0, 0, 0, .15);
|
||||||
|
border-radius: 4px;
|
||||||
|
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
|
||||||
|
box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bootstrap-datetimepicker-widget .list-unstyled {
|
||||||
|
padding-left: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bootstrap-datetimepicker-widget .collapse {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bootstrap-datetimepicker-widget .collapse.in {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fix for bootstrap4 */
|
||||||
|
.bootstrap-datetimepicker-widget .table-condensed > thead > tr > th,
|
||||||
|
.bootstrap-datetimepicker-widget .table-condensed > tbody > tr > td,
|
||||||
|
.bootstrap-datetimepicker-widget .table-condensed > tfoot > tr > td {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
55
static/bootstrap_datepicker_plus/js/datepicker-widget.js
Normal file
55
static/bootstrap_datepicker_plus/js/datepicker-widget.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
jQuery(function ($) {
|
||||||
|
var datepickerDict = {};
|
||||||
|
var isBootstrap4 = $.fn.collapse.Constructor.VERSION.split('.').shift() == "4";
|
||||||
|
function fixMonthEndDate(e, picker) {
|
||||||
|
e.date && picker.val().length && picker.val(e.date.endOf('month').format('YYYY-MM-DD'));
|
||||||
|
}
|
||||||
|
$("[dp_config]:not([disabled])").each(function (i, element) {
|
||||||
|
var $element = $(element), data = {};
|
||||||
|
try {
|
||||||
|
data = JSON.parse($element.attr('dp_config'));
|
||||||
|
}
|
||||||
|
catch (x) { }
|
||||||
|
if (data.id && data.options) {
|
||||||
|
data.$element = $element.datetimepicker(data.options);
|
||||||
|
data.datepickerdata = $element.data("DateTimePicker");
|
||||||
|
datepickerDict[data.id] = data;
|
||||||
|
data.$element.next('.input-group-addon').on('click', function(){
|
||||||
|
data.datepickerdata.show();
|
||||||
|
});
|
||||||
|
if(isBootstrap4){
|
||||||
|
data.$element.on("dp.show", function (e) {
|
||||||
|
$('.collapse.in').addClass('show');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$.each(datepickerDict, function (id, to_picker) {
|
||||||
|
if (to_picker.linked_to) {
|
||||||
|
var from_picker = datepickerDict[to_picker.linked_to];
|
||||||
|
from_picker.datepickerdata.maxDate(to_picker.datepickerdata.date() || false);
|
||||||
|
to_picker.datepickerdata.minDate(from_picker.datepickerdata.date() || false);
|
||||||
|
from_picker.$element.on("dp.change", function (e) {
|
||||||
|
to_picker.datepickerdata.minDate(e.date || false);
|
||||||
|
});
|
||||||
|
to_picker.$element.on("dp.change", function (e) {
|
||||||
|
if (to_picker.picker_type == 'MONTH') fixMonthEndDate(e, to_picker.$element);
|
||||||
|
from_picker.datepickerdata.maxDate(e.date || false);
|
||||||
|
});
|
||||||
|
if (to_picker.picker_type == 'MONTH') {
|
||||||
|
to_picker.$element.on("dp.hide", function (e) {
|
||||||
|
fixMonthEndDate(e, to_picker.$element);
|
||||||
|
});
|
||||||
|
fixMonthEndDate({ date: to_picker.datepickerdata.date() }, to_picker.$element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if(isBootstrap4) {
|
||||||
|
$('body').on('show.bs.collapse','.bootstrap-datetimepicker-widget .collapse',function(e){
|
||||||
|
$(e.target).addClass('in');
|
||||||
|
});
|
||||||
|
$('body').on('hidden.bs.collapse','.bootstrap-datetimepicker-widget .collapse',function(e){
|
||||||
|
$(e.target).removeClass('in');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
6
templates/bootstrap_datepicker_plus/date_picker.html
Normal file
6
templates/bootstrap_datepicker_plus/date_picker.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<div class="input-group date">
|
||||||
|
{% include "bootstrap_datepicker_plus/input.html" %}
|
||||||
|
<div class="input-group-addon input-group-append" data-target="#datetimepicker1" data-toggle="datetimepickerv">
|
||||||
|
<div class="input-group-text"><i class="glyphicon glyphicon-calendar"></i></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
4
templates/bootstrap_datepicker_plus/input.html
Normal file
4
templates/bootstrap_datepicker_plus/input.html
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<input type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None and widget.value != "" %}
|
||||||
|
value="{{ widget.value }}"{% endif %}{% for name, value in widget.attrs.items %}{% ifnotequal value False %}
|
||||||
|
{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}
|
||||||
|
{% endifnotequal %}{% endfor %}/>
|
6
templates/bootstrap_datepicker_plus/time_picker.html
Normal file
6
templates/bootstrap_datepicker_plus/time_picker.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<div class="input-group date">
|
||||||
|
{% include "bootstrap_datepicker_plus/input.html" %}
|
||||||
|
<div class="input-group-addon input-group-append" data-target="#datetimepicker1" data-toggle="datetimepickerv">
|
||||||
|
<div class="input-group-text"><i class="glyphicon glyphicon-time"></i></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
11
templates/note/amount_input.html
Normal file
11
templates/note/amount_input.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<div class="input-group">
|
||||||
|
<input class="form-control mx-auto d-block" type="number" min="0" step="0.01"
|
||||||
|
{% if widget.value != None and widget.value != "" %}value="{{ widget.value }}"{% endif %}
|
||||||
|
name="{{ widget.name }}"
|
||||||
|
{% for name, value in widget.attrs.items %}
|
||||||
|
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
|
||||||
|
{% endfor %}>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<span class="input-group-text">€</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -126,12 +126,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group col-md-6">
|
<div class="form-group col-md-6">
|
||||||
<label for="amount">{% trans "Amount" %} :</label>
|
<label for="amount">{% trans "Amount" %} :</label>
|
||||||
<div class="input-group">
|
{% include "note/amount_input.html" with widget=amount_widget %}
|
||||||
<input class="form-control mx-auto d-block" type="number" min="0" step="0.01" id="amount" />
|
|
||||||
<div class="input-group-append">
|
|
||||||
<span class="input-group-text">€</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group col-md-6">
|
<div class="form-group col-md-6">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load crispy_forms_tags pretty_money %}
|
{% load crispy_forms_tags %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p><a class="btn btn-default" href="{% url 'treasury:invoice_list' %}">{% trans "Invoices list" %}</a></p>
|
<p><a class="btn btn-default" href="{% url 'treasury:invoice_list' %}">{% trans "Invoices list" %}</a></p>
|
||||||
<form method="post" action="">
|
<form method="post" action="">
|
||||||
@ -26,18 +26,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<tr class="row-formset">
|
<tr class="row-formset">
|
||||||
<td>{{ form.designation }}</td>
|
<td>{{ form.designation }}</td>
|
||||||
<td>{{ form.quantity }} </td>
|
<td>{{ form.quantity }}</td>
|
||||||
<td>
|
<td>{{ form.amount }}</td>
|
||||||
{# Use custom input for amount, with the € symbol #}
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="number" name="product_set-{{ forloop.counter0 }}-amount" step="0.01"
|
|
||||||
id="id_product_set-{{ forloop.counter0 }}-amount"
|
|
||||||
value="{{ form.instance.amount|cents_to_euros }}">
|
|
||||||
<div class="input-group-append">
|
|
||||||
<span class="input-group-text">€</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
{# These fields are hidden but handled by the formset to link the id and the invoice id #}
|
{# These fields are hidden but handled by the formset to link the id and the invoice id #}
|
||||||
{{ form.invoice }}
|
{{ form.invoice }}
|
||||||
{{ form.id }}
|
{{ form.id }}
|
||||||
@ -64,15 +54,7 @@
|
|||||||
<tr class="row-formset">
|
<tr class="row-formset">
|
||||||
<td>{{ formset.empty_form.designation }}</td>
|
<td>{{ formset.empty_form.designation }}</td>
|
||||||
<td>{{ formset.empty_form.quantity }} </td>
|
<td>{{ formset.empty_form.quantity }} </td>
|
||||||
<td>
|
<td>{{ formset.empty_form.amount }}</td>
|
||||||
<div class="input-group">
|
|
||||||
<input type="number" name="product_set-__prefix__-amount" step="0.01"
|
|
||||||
id="id_product_set-__prefix__-amount">
|
|
||||||
<div class="input-group-append">
|
|
||||||
<span class="input-group-text">€</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
{{ formset.empty_form.invoice }}
|
{{ formset.empty_form.invoice }}
|
||||||
{{ formset.empty_form.id }}
|
{{ formset.empty_form.id }}
|
||||||
</tr>
|
</tr>
|
||||||
|
Loading…
Reference in New Issue
Block a user