mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-11-04 01:12:08 +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
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user