lino 25.4.5__py3-none-any.whl → 25.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lino/__init__.py +1 -1
- lino/api/doctest.py +14 -0
- lino/core/actions.py +2 -8
- lino/core/actors.py +5 -3
- lino/core/choicelists.py +8 -11
- lino/core/dbtables.py +7 -7
- lino/core/elems.py +16 -11
- lino/core/fields.py +67 -19
- lino/core/kernel.py +6 -0
- lino/core/layouts.py +2 -2
- lino/core/model.py +15 -12
- lino/core/plugin.py +2 -2
- lino/core/site.py +11 -13
- lino/core/store.py +5 -6
- lino/core/tables.py +13 -9
- lino/core/utils.py +4 -23
- lino/help_texts.py +9 -1
- lino/mixins/duplicable.py +9 -3
- lino/mixins/ref.py +9 -0
- lino/mixins/registrable.py +2 -2
- lino/mixins/sequenced.py +12 -8
- lino/modlib/extjs/ext_renderer.py +1 -1
- lino/modlib/gfks/fields.py +1 -1
- lino/modlib/help/models.py +2 -1
- lino/modlib/jinja/renderer.py +1 -0
- lino/modlib/linod/models.py +1 -1
- lino/modlib/notify/fixtures/demo2.py +7 -2
- lino/modlib/periods/fixtures/std.py +1 -3
- lino/modlib/periods/models.py +26 -5
- lino/modlib/publisher/fixtures/synodalworld.py +0 -2
- lino/modlib/weasyprint/__init__.py +1 -0
- lino/modlib/weasyprint/choicelists.py +2 -11
- lino/modlib/weasyprint/config/weasyprint/base.weasy.html +1 -1
- lino/utils/__init__.py +20 -0
- lino/utils/choosers.py +21 -15
- lino/utils/quantities.py +21 -3
- {lino-25.4.5.dist-info → lino-25.5.1.dist-info}/METADATA +1 -1
- {lino-25.4.5.dist-info → lino-25.5.1.dist-info}/RECORD +41 -41
- {lino-25.4.5.dist-info → lino-25.5.1.dist-info}/WHEEL +0 -0
- {lino-25.4.5.dist-info → lino-25.5.1.dist-info}/licenses/AUTHORS.rst +0 -0
- {lino-25.4.5.dist-info → lino-25.5.1.dist-info}/licenses/COPYING +0 -0
lino/core/tables.py
CHANGED
@@ -323,6 +323,12 @@ class AbstractTable(actors.Actor):
|
|
323
323
|
See :ref:`dg.table.default_display_modes`.
|
324
324
|
"""
|
325
325
|
|
326
|
+
parent_layout = None
|
327
|
+
|
328
|
+
@classmethod
|
329
|
+
def attach_to_parent_layout(cls, parent):
|
330
|
+
cls.parent_layout = parent
|
331
|
+
|
326
332
|
@classmethod
|
327
333
|
# def get_display_mode(cls, available_width=None):
|
328
334
|
def get_display_mode(cls):
|
@@ -532,7 +538,7 @@ method in order to sort the rows of the queryset.
|
|
532
538
|
@classmethod
|
533
539
|
def get_column_names(self, ar):
|
534
540
|
"""Dynamic tables can subclass this method and return a value for
|
535
|
-
:attr:`column_names`
|
541
|
+
:attr:`column_names` that depends on the request.
|
536
542
|
|
537
543
|
"""
|
538
544
|
# if settings.SITE.mobile_view:
|
@@ -560,16 +566,14 @@ method in order to sort the rows of the queryset.
|
|
560
566
|
For example, if you have two models :class:`Book` and :class:`Author`,
|
561
567
|
and a foreign key :attr:`Book.author`, which points to the author of the
|
562
568
|
book, and a table `BooksByAuthor` having `master_key` set to
|
563
|
-
``'author'``, then
|
564
|
-
<PK>}` where `<PK>` is the primary key of the action
|
565
|
-
:attr:`master_instance
|
569
|
+
``'author'``, then :meth:`BooksByAuthor.get_filter_kw` would return a
|
570
|
+
dict `{'author': <PK>}` where `<PK>` is the primary key of the action
|
571
|
+
request's :attr:`master_instance
|
566
572
|
<lino.core.requests.BaseRequest.master_instance>`.
|
567
573
|
|
568
|
-
Another example is
|
569
|
-
|
570
|
-
|
571
|
-
detail of a session we want to display a table of related blog
|
572
|
-
entries.
|
574
|
+
Another example is :class:`lino_xl.lib.tickets.EntriesBySession`, where
|
575
|
+
blog entries are not directly linked to a session, but in the detail of
|
576
|
+
a session we want to display a table of related blog entries.
|
573
577
|
|
574
578
|
:class:`lino_xl.lib.households.SiblingsByPerson` Household
|
575
579
|
members are not directly linked to a Person, but usually a
|
lino/core/utils.py
CHANGED
@@ -6,19 +6,19 @@ A collection of utilities which require Django settings to be
|
|
6
6
|
importable.
|
7
7
|
"""
|
8
8
|
|
9
|
-
from .exceptions import ChangedAPI
|
10
9
|
from lino.utils import IncompleteDate
|
11
10
|
import copy
|
12
11
|
import sys
|
13
12
|
import datetime
|
14
13
|
# import yaml
|
15
|
-
|
14
|
+
from importlib import import_module
|
16
15
|
from django.utils.html import format_html, mark_safe, SafeString
|
17
16
|
from django.db import models
|
18
17
|
from django.db.models import Q
|
19
18
|
from django.core.exceptions import FieldDoesNotExist
|
20
19
|
# from django.utils.functional import lazy
|
21
|
-
from
|
20
|
+
from django.core.validators import validate_email, ValidationError, URLValidator
|
21
|
+
from django.apps import apps
|
22
22
|
from django.utils.translation import gettext as _
|
23
23
|
from django.conf import settings
|
24
24
|
from django.core import exceptions
|
@@ -28,9 +28,7 @@ from lino.utils.html import E, assert_safe, tostring
|
|
28
28
|
from lino.utils import capture_output
|
29
29
|
from lino.utils.ranges import isrange
|
30
30
|
|
31
|
-
from
|
32
|
-
|
33
|
-
from django.apps import apps
|
31
|
+
from .exceptions import ChangedAPI
|
34
32
|
|
35
33
|
get_models = apps.get_models
|
36
34
|
|
@@ -642,23 +640,6 @@ def navinfo(qs, elem, limit=None):
|
|
642
640
|
)
|
643
641
|
|
644
642
|
|
645
|
-
# class Handle(object):
|
646
|
-
# """Base class for :class:`lino.core.tables.TableHandle`,
|
647
|
-
# :class:`lino.core.frames.FrameHandle` etc.
|
648
|
-
|
649
|
-
# The "handle" of an actor is responsible for expanding layouts into
|
650
|
-
# sets of (renderer-specific) widgets (called "elements"). This
|
651
|
-
# operation is done once per actor per renderer.
|
652
|
-
|
653
|
-
# """
|
654
|
-
# # def __init__(self):
|
655
|
-
# # self.ui = settings.SITE.kernel.default_ui
|
656
|
-
|
657
|
-
# def setup(self, ar):
|
658
|
-
# settings.SITE.kernel.setup_handle(self, ar)
|
659
|
-
# # self.ui.setup_handle(self, ar)
|
660
|
-
|
661
|
-
|
662
643
|
class Parametrizable(object):
|
663
644
|
"""
|
664
645
|
Base class for both Actors and Actions. See :doc:`/dev/parameters`.
|
lino/help_texts.py
CHANGED
@@ -32,7 +32,7 @@ help_texts = {
|
|
32
32
|
'lino.mixins.dupable.SimilarObjects' : _("""Shows the other objects that are similar to this one."""),
|
33
33
|
'lino.mixins.duplicable.Duplicate' : _("""Duplicate the selected row."""),
|
34
34
|
'lino.mixins.duplicable.Duplicate.run_from_ui' : _("""This actually runs the action."""),
|
35
|
-
'lino.mixins.duplicable.Duplicable' : _("""Adds a row action “Duplicate
|
35
|
+
'lino.mixins.duplicable.Duplicable' : _("""Adds a row action “Duplicate”, which duplicates (creates a clone of) the object it was called on."""),
|
36
36
|
'lino.mixins.human.Human' : _("""Base class for models that represent a human."""),
|
37
37
|
'lino.mixins.human.Human.title' : _("""Used to specify a professional position or academic qualification like “Dr.” or “PhD”."""),
|
38
38
|
'lino.mixins.human.Human.first_name' : _("""The first name, also known as given name."""),
|
@@ -70,6 +70,7 @@ help_texts = {
|
|
70
70
|
'lino.mixins.ref.Referrable.ref' : _("""The reference. This must be either empty or unique."""),
|
71
71
|
'lino.mixins.ref.Referrable.ref_max_length' : _("""The preferred width of the ref field."""),
|
72
72
|
'lino.mixins.ref.Referrable.on_duplicate' : _("""Before saving a duplicated object for the first time, we must change the ref in order to avoid an IntegrityError."""),
|
73
|
+
'lino.mixins.ref.Referrable.get_next_row' : _("""Return the next database row, or None if this is the last one."""),
|
73
74
|
'lino.mixins.ref.Referrable.get_by_ref' : _("""Return the object identified by the given reference."""),
|
74
75
|
'lino.mixins.ref.Referrable.quick_search_filter' : _("""Overrides the default behaviour defined in lino.core.model.Model.quick_search_filter(). For Referrable objects, when quick-searching for a text containing only digits, the user usually means the ref and not the primary key."""),
|
75
76
|
'lino.mixins.ref.StructuredReferrable' : _("""A referrable whose ref field is used to define a hierarchical structure."""),
|
@@ -97,6 +98,7 @@ help_texts = {
|
|
97
98
|
'lino.mixins.sequenced.Sequenced.get_siblings' : _("""Return a Django Queryset with all siblings of this, or None if this is a root element which cannot have any siblings."""),
|
98
99
|
'lino.mixins.sequenced.Sequenced.set_seqno' : _("""Initialize seqno to the seqno of eldest sibling + 1."""),
|
99
100
|
'lino.mixins.sequenced.Sequenced.seqno_changed' : _("""If the user manually assigns a seqno."""),
|
101
|
+
'lino.mixins.sequenced.Sequenced.dndreorder' : _("""A place holder column for drag and drop row reorder on React front end"""),
|
100
102
|
'lino.mixins.sequenced.Hierarchical' : _("""Model mixin for things that have a “parent” and “siblings”."""),
|
101
103
|
'lino.mixins.sequenced.Hierarchical.children_summary' : _("""A comma-separated list of the children."""),
|
102
104
|
'lino.mixins.sequenced.Hierarchical.get_parental_line' : _("""Return an ordered list of all ancestors of this instance."""),
|
@@ -320,6 +322,11 @@ help_texts = {
|
|
320
322
|
'lino.core.model.Model.set_widget_options' : _("""Set default values for the widget options of a given element."""),
|
321
323
|
'lino.core.model.Model.get_overview_elems' : _("""Return a list of HTML elements to be shown in overview field."""),
|
322
324
|
'lino.core.model.Model.merge_row' : _("""Merge this object into another object of same class."""),
|
325
|
+
'lino.utils.quantities.Quantity' : _("""The base class for all quantities."""),
|
326
|
+
'lino.utils.quantities.Quantity.limit_length' : _("""Reduce the number of decimal places so that the value fits into a field of the specified max_length, if possible. This will round the value (reducing precision) if needed."""),
|
327
|
+
'lino.utils.quantities.Duration' : _("""The class to represent a duration."""),
|
328
|
+
'lino.utils.quantities.Percentage' : _("""The class to represent a percentage."""),
|
329
|
+
'lino.utils.quantities.Fraction' : _("""The class to represent a fraction. (Not yet implemented)"""),
|
323
330
|
'lino.core.model.Model.get_request_queryset' : _("""Return the Django queryset to be used by action request ar for any data table on this model."""),
|
324
331
|
'lino.core.model.Model.before_ui_save' : _("""A hook for adding custom code to be executed each time an instance of this model gets updated via the user interface and before the changes are written to the database."""),
|
325
332
|
'lino.core.model.Model.after_ui_save' : _("""Like before_ui_save(), but is called after the changes are written to the database."""),
|
@@ -709,4 +716,5 @@ help_texts = {
|
|
709
716
|
'lino.core.model.Model.create_FOO_choice' : _("""For every field named “FOO” for which a chooser exists, if the model also has a method called “create_FOO_choice”, then this chooser will be a learning chooser. That is, users can enter text into the combobox, and Lino will create a new database object from it."""),
|
710
717
|
'lino.core.model.Model.get_choices_text' : _("""Return the text to be displayed when an instance of this model is being used as a choice in a combobox of a ForeignKey field pointing to this model. request is the web request, actor is the requesting actor."""),
|
711
718
|
'lino.core.model.Model.disable_delete' : _("""Decide whether this database object may be deleted. Return None when there is no veto against deleting this database row, otherwise a translatable message that explains to the user why they can’t delete this row."""),
|
719
|
+
'lino.core.model.Model.update_field' : _("""Shortcut to call lino.core.inject.update_field() for usage during lino.core.site.Site.do_site_startup() in a settings.py or similar place."""),
|
712
720
|
}
|
lino/mixins/duplicable.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# -*- coding: UTF-8 -*-
|
2
|
-
# Copyright 2012-
|
2
|
+
# Copyright 2012-2025 Rumma & Ko Ltd
|
3
3
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
4
|
"""Defines the model mixin :class:`Duplicable`. "duplicable"
|
5
5
|
[du'plikəblə] means "able to produce a duplicate
|
@@ -9,10 +9,11 @@
|
|
9
9
|
|
10
10
|
from django.utils.translation import gettext_lazy as _
|
11
11
|
|
12
|
+
from lino import logger
|
12
13
|
from lino.core import actions
|
13
14
|
from lino.core import model
|
14
15
|
from lino.core.diff import ChangeWatcher
|
15
|
-
from lino.core.roles import Expert
|
16
|
+
# from lino.core.roles import Expert
|
16
17
|
|
17
18
|
|
18
19
|
class Duplicate(actions.Action):
|
@@ -73,6 +74,7 @@ class Duplicate(actions.Action):
|
|
73
74
|
new.full_clean()
|
74
75
|
new.save(force_insert=True)
|
75
76
|
cw = ChangeWatcher(new)
|
77
|
+
relcount = 0
|
76
78
|
|
77
79
|
for fk, qs in related:
|
78
80
|
for relobj in qs:
|
@@ -80,6 +82,7 @@ class Duplicate(actions.Action):
|
|
80
82
|
setattr(relobj, fk.name, new)
|
81
83
|
relobj.on_duplicate(ar, new)
|
82
84
|
relobj.save(force_insert=True)
|
85
|
+
relcount += 1
|
83
86
|
|
84
87
|
new.after_duplicate(ar, obj)
|
85
88
|
|
@@ -87,6 +90,9 @@ class Duplicate(actions.Action):
|
|
87
90
|
new.full_clean()
|
88
91
|
new.save()
|
89
92
|
|
93
|
+
logger.info("%s has been duplicated to %s (%d related rows)",
|
94
|
+
obj, new, relcount)
|
95
|
+
|
90
96
|
return new
|
91
97
|
|
92
98
|
def run_from_ui(self, ar, **kw):
|
@@ -115,7 +121,7 @@ class Duplicate(actions.Action):
|
|
115
121
|
|
116
122
|
|
117
123
|
class Duplicable(model.Model):
|
118
|
-
"""Adds a row action "Duplicate" which duplicates (creates a clone
|
124
|
+
"""Adds a row action "Duplicate", which duplicates (creates a clone
|
119
125
|
of) the object it was called on.
|
120
126
|
|
121
127
|
Subclasses may override :meth:`lino.core.model.Model.on_duplicate`
|
lino/mixins/ref.py
CHANGED
@@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _
|
|
10
10
|
from django.db.models.functions import Length
|
11
11
|
|
12
12
|
from lino.utils.html import E
|
13
|
+
from lino.utils import nextref
|
13
14
|
from lino.core import model
|
14
15
|
from lino.core.fields import displayfield
|
15
16
|
|
@@ -61,6 +62,14 @@ class Referrable(model.Model):
|
|
61
62
|
def __str__(self):
|
62
63
|
return self.ref or super().__str__()
|
63
64
|
|
65
|
+
def get_next_row(self):
|
66
|
+
"""Return the next database row, or None if this is the last one.
|
67
|
+
"""
|
68
|
+
ref = nextref(self.ref)
|
69
|
+
if ref is None:
|
70
|
+
return None
|
71
|
+
return self.__class__.get_by_ref(ref, None)
|
72
|
+
|
64
73
|
@staticmethod
|
65
74
|
def ref_prefix(obj, ar=None):
|
66
75
|
return obj.as_ref_prefix(ar)
|
lino/mixins/registrable.py
CHANGED
@@ -37,7 +37,7 @@ class Registrable(model.Model):
|
|
37
37
|
Base class to anything that may be "registered" and "deregistered", where
|
38
38
|
"registered" means "this object has been taken account of".
|
39
39
|
|
40
|
-
For example, when a :term:`
|
40
|
+
For example, when a :term:`numbered voucher` is registered, its associated
|
41
41
|
:term:`ledger movements <ledger movement>` have been generated.
|
42
42
|
Deregistering a voucher will first delete these movements.
|
43
43
|
|
@@ -45,7 +45,7 @@ class Registrable(model.Model):
|
|
45
45
|
usually limited to certain fields.
|
46
46
|
|
47
47
|
:class:`lino_xl.lib.cal.Reservation` is an example of a registrable that is
|
48
|
-
not a
|
48
|
+
not a numbered voucher.
|
49
49
|
|
50
50
|
Subclasses must themselves define a field :attr:`state`.
|
51
51
|
|
lino/mixins/sequenced.py
CHANGED
@@ -279,15 +279,11 @@ class Sequenced(Duplicable):
|
|
279
279
|
Initialize `seqno` to the `seqno` of eldest sibling + 1.
|
280
280
|
"""
|
281
281
|
qs = self.get_siblings().order_by("seqno")
|
282
|
-
if qs
|
283
|
-
self.seqno =
|
282
|
+
if (count := qs.count()) == 0:
|
283
|
+
self.seqno = 1
|
284
284
|
else:
|
285
|
-
|
286
|
-
|
287
|
-
self.seqno = 1
|
288
|
-
else:
|
289
|
-
last = qs[n - 1]
|
290
|
-
self.seqno = last.seqno + 1
|
285
|
+
last = qs[count - 1]
|
286
|
+
self.seqno = last.seqno + 1
|
291
287
|
|
292
288
|
def full_clean(self, *args, **kw):
|
293
289
|
if not self.seqno:
|
@@ -329,6 +325,14 @@ class Sequenced(Duplicable):
|
|
329
325
|
message=_("Renumbered {} of {} siblings.").format(n, qs.count()))
|
330
326
|
ar.set_response(refresh_all=True)
|
331
327
|
|
328
|
+
@fields.displayfield("⇵")
|
329
|
+
def dndreorder(self, ar):
|
330
|
+
"""A place holder column for drag and drop row reorder on :term:`React front end`
|
331
|
+
|
332
|
+
CAUTION: Do NOT rename this field, for react works on checking the name as dndreorder.
|
333
|
+
"""
|
334
|
+
return None
|
335
|
+
|
332
336
|
@fields.displayfield(_("Move"))
|
333
337
|
def move_buttons(obj, ar):
|
334
338
|
if ar is None:
|
@@ -96,7 +96,7 @@ class ExtRenderer(JsCacheRenderer):
|
|
96
96
|
super().__init__(plugin)
|
97
97
|
jsgen.register_converter(self.py2js_converter)
|
98
98
|
|
99
|
-
for s in
|
99
|
+
for s in ('green', 'blue', 'red', 'yellow', 'lightgrey'):
|
100
100
|
self.row_classes_map[s] = "x-grid3-row-%s" % s
|
101
101
|
|
102
102
|
self.prepare_layouts()
|
lino/modlib/gfks/fields.py
CHANGED
@@ -5,7 +5,6 @@
|
|
5
5
|
from django.db import models
|
6
6
|
from django.conf import settings
|
7
7
|
|
8
|
-
from lino.utils.choosers import chooser
|
9
8
|
from lino.core.utils import full_model_name
|
10
9
|
|
11
10
|
if settings.SITE.is_installed("contenttypes"):
|
@@ -31,6 +30,7 @@ if settings.SITE.is_installed("contenttypes"):
|
|
31
30
|
def contribute_to_class(self, cls, name, **kwargs):
|
32
31
|
# Automatically setup chooser and display field for ID field of
|
33
32
|
# generic foreign key.
|
33
|
+
from lino.utils.choosers import chooser
|
34
34
|
|
35
35
|
super().contribute_to_class(cls, name, **kwargs)
|
36
36
|
|
lino/modlib/help/models.py
CHANGED
@@ -6,7 +6,6 @@ from django.utils.translation import get_language
|
|
6
6
|
from lino.core.actors import Actor
|
7
7
|
from lino.api import dd, _
|
8
8
|
from lino.modlib.memo.mixins import MemoReferrable
|
9
|
-
from lino_xl.lib.contacts.mixins import ContactRelated
|
10
9
|
|
11
10
|
use_contacts = dd.get_plugin_setting("help", "use_contacts")
|
12
11
|
make_help_pages = dd.get_plugin_setting("help", "make_help_pages")
|
@@ -61,6 +60,8 @@ if make_help_pages:
|
|
61
60
|
|
62
61
|
if use_contacts:
|
63
62
|
|
63
|
+
from lino_xl.lib.contacts.mixins import ContactRelated
|
64
|
+
|
64
65
|
class SiteContactTypes(dd.ChoiceList):
|
65
66
|
verbose_name = _("Site contact type")
|
66
67
|
verbose_name_plural = _("Site contact types")
|
lino/modlib/jinja/renderer.py
CHANGED
lino/modlib/linod/models.py
CHANGED
@@ -32,7 +32,7 @@ class SystemTasks(dd.Table):
|
|
32
32
|
model = "linod.SystemTask"
|
33
33
|
order_by = ['seqno']
|
34
34
|
required_roles = dd.login_required(SiteStaff)
|
35
|
-
column_names = "seqno name log_level disabled status procedure *"
|
35
|
+
column_names = "seqno name log_level disabled status procedure dndreorder *"
|
36
36
|
detail_layout = """
|
37
37
|
seqno procedure
|
38
38
|
name
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2016-
|
1
|
+
# Copyright 2016-2025 Rumma & Ko Ltd
|
2
2
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
3
3
|
"""Emit a broadcast notification "The database has been initialized."
|
4
4
|
|
@@ -14,7 +14,12 @@ from django.utils.timezone import make_aware
|
|
14
14
|
|
15
15
|
|
16
16
|
def objects():
|
17
|
-
|
17
|
+
|
18
|
+
# The messages are dated one day before today() because
|
19
|
+
# book/docs/specs/notify.rst has a code snippet that failed when pm prep had
|
20
|
+
# been run before 5:48am
|
21
|
+
|
22
|
+
now = datetime.datetime.combine(dd.today(-1), i2t(548))
|
18
23
|
if settings.USE_TZ:
|
19
24
|
now = make_aware(now)
|
20
25
|
mt = rt.models.notify.MessageTypes.system
|
@@ -8,16 +8,14 @@ from lino.api import dd, rt, _
|
|
8
8
|
|
9
9
|
start_year = dd.get_plugin_setting("periods", "start_year", None)
|
10
10
|
|
11
|
+
|
11
12
|
def objects():
|
12
13
|
StoredYear = rt.models.periods.StoredYear
|
13
14
|
|
14
|
-
cfg = dd.plugins.periods
|
15
15
|
site = settings.SITE
|
16
16
|
if site.the_demo_date is not None:
|
17
17
|
if start_year > site.the_demo_date.year:
|
18
18
|
raise Exception("plugins.periods.start_year is after the_demo_date")
|
19
19
|
today = site.the_demo_date or datetime.date.today()
|
20
20
|
for y in range(start_year, today.year + 6):
|
21
|
-
# yield StoredYear.create_from_year(y)
|
22
21
|
yield StoredYear.get_or_create_from_date(datetime.date(y, today.month, today.day))
|
23
|
-
# StoredYears.add_item(StoredYear.year2value(y), str(y))
|
lino/modlib/periods/models.py
CHANGED
@@ -19,7 +19,6 @@ NEXT_YEAR_SEP = "/"
|
|
19
19
|
YEAR_PERIOD_SEP = "-"
|
20
20
|
|
21
21
|
|
22
|
-
|
23
22
|
class StoredYear(DateRange, Referrable):
|
24
23
|
|
25
24
|
class Meta:
|
@@ -30,7 +29,7 @@ class StoredYear(DateRange, Referrable):
|
|
30
29
|
|
31
30
|
preferred_foreignkey_width = 10
|
32
31
|
|
33
|
-
state = PeriodStates.field(
|
32
|
+
state = PeriodStates.field(blank=True)
|
34
33
|
|
35
34
|
@classmethod
|
36
35
|
def get_simple_parameters(cls):
|
@@ -80,9 +79,23 @@ class StoredYear(DateRange, Referrable):
|
|
80
79
|
obj.save()
|
81
80
|
return obj
|
82
81
|
|
82
|
+
def full_clean(self, *args, **kwargs):
|
83
|
+
if not self.state:
|
84
|
+
if self.start_date.year + 1 < dd.today().year:
|
85
|
+
self.state = PeriodStates.closed
|
86
|
+
else:
|
87
|
+
self.state = PeriodStates.open
|
88
|
+
super().full_clean(*args, **kwargs)
|
89
|
+
|
83
90
|
def __str__(self):
|
84
91
|
return self.ref
|
85
92
|
|
93
|
+
def get_next_row(self):
|
94
|
+
nextyear = self.start_date.replace(year=self.start_date.year+1)
|
95
|
+
ref = self.__class__.get_ref_for_date(nextyear)
|
96
|
+
return self.__class__.get_by_ref(ref, None)
|
97
|
+
# return self.__class__.get_or_create_from_date(nextyear)
|
98
|
+
|
86
99
|
|
87
100
|
class StoredPeriod(DateRange, Referrable):
|
88
101
|
|
@@ -94,8 +107,9 @@ class StoredPeriod(DateRange, Referrable):
|
|
94
107
|
|
95
108
|
preferred_foreignkey_width = 10
|
96
109
|
|
97
|
-
state = PeriodStates.field(
|
98
|
-
year = dd.ForeignKey('periods.StoredYear', blank=True,
|
110
|
+
state = PeriodStates.field(blank=True)
|
111
|
+
year = dd.ForeignKey('periods.StoredYear', blank=True,
|
112
|
+
null=True, related_name="periods")
|
99
113
|
remark = models.CharField(_("Remark"), max_length=250, blank=True)
|
100
114
|
|
101
115
|
@classmethod
|
@@ -107,7 +121,8 @@ class StoredPeriod(DateRange, Referrable):
|
|
107
121
|
@classmethod
|
108
122
|
def get_request_queryset(cls, ar):
|
109
123
|
qs = super().get_request_queryset(ar)
|
110
|
-
if (pv := ar.param_values) is None:
|
124
|
+
if (pv := ar.param_values) is None:
|
125
|
+
return qs
|
111
126
|
|
112
127
|
# if pv.start_date is None or pv.end_date is None:
|
113
128
|
# period = None
|
@@ -206,6 +221,8 @@ class StoredPeriod(DateRange, Referrable):
|
|
206
221
|
self.start_date = dd.today().replace(day=1)
|
207
222
|
if not self.year_id:
|
208
223
|
self.year = StoredYear.get_or_create_from_date(self.start_date)
|
224
|
+
if not self.state:
|
225
|
+
self.state = self.year.state
|
209
226
|
super().full_clean(*args, **kwargs)
|
210
227
|
|
211
228
|
def __str__(self):
|
@@ -230,11 +247,14 @@ class StoredPeriod(DateRange, Referrable):
|
|
230
247
|
return parts[1]
|
231
248
|
return self.ref
|
232
249
|
|
250
|
+
|
233
251
|
StoredPeriod.set_widget_options('ref', width=6)
|
234
252
|
|
253
|
+
|
235
254
|
def date2ref(d):
|
236
255
|
return StoredYear.get_ref_for_date(d) + YEAR_PERIOD_SEP + StoredPeriod.get_ref_for_date(d)
|
237
256
|
|
257
|
+
|
238
258
|
class StoredYears(dd.Table):
|
239
259
|
model = 'periods.StoredYear'
|
240
260
|
required_roles = dd.login_required(OfficeStaff)
|
@@ -244,6 +264,7 @@ class StoredYears(dd.Table):
|
|
244
264
|
# start_date end_date
|
245
265
|
# """
|
246
266
|
|
267
|
+
|
247
268
|
class StoredPeriods(dd.Table):
|
248
269
|
required_roles = dd.login_required(OfficeStaff)
|
249
270
|
model = 'periods.StoredPeriod'
|
@@ -2,9 +2,9 @@
|
|
2
2
|
# Copyright 2016-2024 Rumma & Ko Ltd
|
3
3
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
4
4
|
|
5
|
-
import os
|
6
5
|
from pathlib import Path
|
7
|
-
from
|
6
|
+
from lino.modlib.jinja.choicelists import JinjaBuildMethod
|
7
|
+
from lino.modlib.printing.choicelists import BuildMethods
|
8
8
|
|
9
9
|
try:
|
10
10
|
from weasyprint import HTML
|
@@ -20,15 +20,6 @@ except ImportError:
|
|
20
20
|
BULMA_CSS = None
|
21
21
|
|
22
22
|
|
23
|
-
|
24
|
-
from django.conf import settings
|
25
|
-
from django.utils import translation
|
26
|
-
|
27
|
-
from lino.api import dd
|
28
|
-
from lino.modlib.jinja.choicelists import JinjaBuildMethod
|
29
|
-
from lino.modlib.printing.choicelists import BuildMethods
|
30
|
-
|
31
|
-
|
32
23
|
class WeasyBuildMethod(JinjaBuildMethod):
|
33
24
|
template_ext = ".weasy.html"
|
34
25
|
templates_name = "weasy"
|
@@ -77,7 +77,7 @@ div.recipient {
|
|
77
77
|
size: {%- block pagesize %}landscape{%- endblock %};
|
78
78
|
margin: {{dd.plugins.weasyprint.margin}}mm;
|
79
79
|
margin-bottom: {{dd.plugins.weasyprint.margin+dd.plugins.weasyprint.footer_height}}mm;
|
80
|
-
{%- if dd.plugins.weasyprint.top_right_image -%}
|
80
|
+
{%- if True or dd.plugins.weasyprint.top_right_image -%}
|
81
81
|
margin-top: {{dd.plugins.weasyprint.margin+dd.plugins.weasyprint.header_height}}mm;
|
82
82
|
{%- endif -%}
|
83
83
|
margin-left: {{dd.plugins.weasyprint.margin_left}}mm;
|
lino/utils/__init__.py
CHANGED
@@ -655,3 +655,23 @@ def logging_disabled(level):
|
|
655
655
|
yield
|
656
656
|
finally:
|
657
657
|
logging.disable(logging.NOTSET)
|
658
|
+
|
659
|
+
|
660
|
+
def nextref(ref):
|
661
|
+
"""
|
662
|
+
Increment the first number found in the string, preserving everything
|
663
|
+
thereafter.
|
664
|
+
|
665
|
+
Tested examples in :doc:`/topics/utils`.
|
666
|
+
"""
|
667
|
+
num = ""
|
668
|
+
suffix = ""
|
669
|
+
for i, c in enumerate(ref):
|
670
|
+
if c.isdigit():
|
671
|
+
num += c
|
672
|
+
else:
|
673
|
+
suffix = ref[i:]
|
674
|
+
break
|
675
|
+
if not num:
|
676
|
+
return None
|
677
|
+
return str(int(num)+1) + suffix
|
lino/utils/choosers.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2009-
|
1
|
+
# Copyright 2009-2025 Rumma & Ko Ltd
|
2
2
|
# License: GNU Affero General Public License v3 (see file COPYING for details)
|
3
3
|
"""Extends the possibilities for defining choices for fields of a
|
4
4
|
Django model.
|
@@ -13,19 +13,18 @@ Example values in :doc:`/topics/utils`.
|
|
13
13
|
|
14
14
|
"""
|
15
15
|
|
16
|
+
import re
|
16
17
|
import decimal
|
17
18
|
import datetime
|
18
|
-
from dateutil import parser as dateparser
|
19
|
-
|
20
|
-
import re
|
21
19
|
|
22
|
-
|
23
|
-
r'^<a href="javascript:Lino\.(\w+\.\w+)\.detail\.run\(.*,\{ "record_id": (\w+) \}\)">.*</a>$'
|
24
|
-
)
|
25
|
-
|
26
|
-
from django.db import models
|
27
|
-
from django.conf import settings
|
20
|
+
from dateutil import parser as dateparser
|
28
21
|
from django.core.exceptions import BadRequest
|
22
|
+
from django.conf import settings
|
23
|
+
from django.db import models
|
24
|
+
|
25
|
+
from lino.core.utils import getrqdata
|
26
|
+
from lino.core import fields
|
27
|
+
from lino.core import constants
|
29
28
|
|
30
29
|
try:
|
31
30
|
from django.contrib.contenttypes.models import ContentType
|
@@ -33,10 +32,9 @@ except RuntimeError:
|
|
33
32
|
pass
|
34
33
|
# Happens when the site doesn't use contenttypes. Ignore silently.
|
35
34
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
from lino.core.utils import getrqdata
|
35
|
+
GFK_HACK = re.compile(
|
36
|
+
r'^<a href="javascript:Lino\.(\w+\.\w+)\.detail\.run\(.*,\{ "record_id": (\w+) \}\)">.*</a>$'
|
37
|
+
)
|
40
38
|
|
41
39
|
# class DataError(Exception):
|
42
40
|
# pass
|
@@ -257,6 +255,11 @@ class Chooser(FieldChooser):
|
|
257
255
|
if isinstance(field, fields.VirtualField):
|
258
256
|
selector = field.return_type
|
259
257
|
|
258
|
+
# TODO: move this to top and fix ImportError cannot import name
|
259
|
+
# 'chooser' from partially initialized module 'lino.utils.choosers'
|
260
|
+
# (most likely due to a circular import)
|
261
|
+
from lino.modlib.gfks.fields import GenericForeignKeyIdField
|
262
|
+
|
260
263
|
if isinstance(selector, ChoiceListField):
|
261
264
|
self.simple_values = getattr(meth, "simple_values", False)
|
262
265
|
self.instance_values = getattr(meth, "instance_values", True)
|
@@ -265,6 +268,8 @@ class Chooser(FieldChooser):
|
|
265
268
|
)
|
266
269
|
elif is_foreignkey(selector):
|
267
270
|
pass
|
271
|
+
elif isinstance(selector, GenericForeignKeyIdField):
|
272
|
+
pass
|
268
273
|
# elif isinstance(field, fields.VirtualField) and isinstance(
|
269
274
|
# field.return_type, models.ForeignKey
|
270
275
|
# ):
|
@@ -471,7 +476,7 @@ def _chooser(make, **options):
|
|
471
476
|
return fn(*args)
|
472
477
|
|
473
478
|
cp = options.pop(
|
474
|
-
"context_params", fn.__code__.co_varnames[1
|
479
|
+
"context_params", fn.__code__.co_varnames[1: fn.__code__.co_argcount]
|
475
480
|
)
|
476
481
|
wrapped.context_params = cp
|
477
482
|
for k, v in options.items():
|
@@ -518,6 +523,7 @@ def check_for_chooser(holder, field):
|
|
518
523
|
methname = field.name + "_choices"
|
519
524
|
m = getattr(holder, methname, None)
|
520
525
|
if m is not None:
|
526
|
+
# print(f"20250511 chooser for {field.name} in {holder} is {m}")
|
521
527
|
# if field.name.endswith('municipality'):
|
522
528
|
# print("20200524 check_for_chooser() found {} on {}", field.name, holder)
|
523
529
|
# 20200425 fix theoretical bug
|