mirror of
https://gitlab.crans.org/bde/nk20
synced 2024-12-22 23:42:25 +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
|
||||
|
||||
from django import forms
|
||||
|
||||
from activity.models import Activity
|
||||
from note_kfet.inputs import DateTimePickerInput
|
||||
|
||||
|
||||
class ActivityForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Activity
|
||||
fields = '__all__'
|
||||
widgets = {
|
||||
"date_start": DateTimePickerInput(),
|
||||
"date_end": DateTimePickerInput(),
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# 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.utils.translation import gettext_lazy as _
|
||||
from django_tables2.views import SingleTableView
|
||||
@ -9,12 +10,12 @@ from .forms import ActivityForm
|
||||
from .models import Activity
|
||||
|
||||
|
||||
class ActivityCreateView(CreateView):
|
||||
class ActivityCreateView(LoginRequiredMixin, CreateView):
|
||||
model = Activity
|
||||
form_class = ActivityForm
|
||||
|
||||
|
||||
class ActivityListView(SingleTableView):
|
||||
class ActivityListView(LoginRequiredMixin, SingleTableView):
|
||||
model = Activity
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -25,14 +26,14 @@ class ActivityListView(SingleTableView):
|
||||
return ctx
|
||||
|
||||
|
||||
class ActivityDetailView(DetailView):
|
||||
class ActivityDetailView(LoginRequiredMixin, DetailView):
|
||||
model = Activity
|
||||
|
||||
|
||||
class ActivityUpdateView(UpdateView):
|
||||
class ActivityUpdateView(LoginRequiredMixin, UpdateView):
|
||||
model = Activity
|
||||
form_class = ActivityForm
|
||||
|
||||
|
||||
class ActivityEntryView(TemplateView):
|
||||
class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||
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.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_tables2 import SingleTableView
|
||||
from django.urls import reverse_lazy
|
||||
from note_kfet.inputs import AmountInput
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
from .forms import TransactionTemplateForm
|
||||
@ -40,6 +41,7 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = _('Transfer money')
|
||||
context['amount_widget'] = AmountInput(attrs={"id": "amount"})
|
||||
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
|
||||
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
|
||||
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 django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note_kfet.inputs import DatePickerInput, AmountInput
|
||||
|
||||
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
|
||||
date = forms.DateField(
|
||||
initial=datetime.date.today,
|
||||
widget=forms.TextInput(attrs={'type': 'date'})
|
||||
widget=DatePickerInput()
|
||||
)
|
||||
|
||||
def clean_date(self):
|
||||
@ -30,12 +31,21 @@ class InvoiceForm(forms.ModelForm):
|
||||
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
|
||||
# its products. The FormSet will search automatically the ForeignKey in the Product model.
|
||||
ProductFormSet = forms.inlineformset_factory(
|
||||
Invoice,
|
||||
Product,
|
||||
fields='__all__',
|
||||
form=ProductForm,
|
||||
extra=1,
|
||||
)
|
||||
|
||||
|
@ -50,18 +50,8 @@ class InvoiceCreateView(LoginRequiredMixin, CreateView):
|
||||
def form_valid(self, 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
|
||||
formset = ProductFormSet(kwargs, instance=form.instance)
|
||||
formset = ProductFormSet(self.request.POST, instance=form.instance)
|
||||
if formset.is_valid():
|
||||
for f in formset:
|
||||
# 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):
|
||||
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
|
||||
|
||||
formset = ProductFormSet(kwargs, instance=form.instance)
|
||||
formset = ProductFormSet(self.request.POST, instance=form.instance)
|
||||
saved = []
|
||||
# For each product, we save it
|
||||
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.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.forms',
|
||||
# API
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
@ -100,6 +101,8 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
|
||||
|
||||
WSGI_APPLICATION = 'note_kfet.wsgi.application'
|
||||
|
||||
# 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-group col-md-6">
|
||||
<label for="amount">{% trans "Amount" %} :</label>
|
||||
<div class="input-group">
|
||||
<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>
|
||||
{% include "note/amount_input.html" with widget=amount_widget %}
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-6">
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags pretty_money %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% block content %}
|
||||
<p><a class="btn btn-default" href="{% url 'treasury:invoice_list' %}">{% trans "Invoices list" %}</a></p>
|
||||
<form method="post" action="">
|
||||
@ -26,18 +26,8 @@
|
||||
{% endif %}
|
||||
<tr class="row-formset">
|
||||
<td>{{ form.designation }}</td>
|
||||
<td>{{ form.quantity }} </td>
|
||||
<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>
|
||||
<td>{{ form.quantity }}</td>
|
||||
<td>{{ form.amount }}</td>
|
||||
{# These fields are hidden but handled by the formset to link the id and the invoice id #}
|
||||
{{ form.invoice }}
|
||||
{{ form.id }}
|
||||
@ -64,15 +54,7 @@
|
||||
<tr class="row-formset">
|
||||
<td>{{ formset.empty_form.designation }}</td>
|
||||
<td>{{ formset.empty_form.quantity }} </td>
|
||||
<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>
|
||||
<td>{{ formset.empty_form.amount }}</td>
|
||||
{{ formset.empty_form.invoice }}
|
||||
{{ formset.empty_form.id }}
|
||||
</tr>
|
||||
|
Loading…
Reference in New Issue
Block a user