mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-31 15:50:03 +01:00 
			
		
		
		
	Use custom inputs for date picker and amounts
This commit is contained in:
		| @@ -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> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user