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.
Files changed (41) hide show
  1. lino/__init__.py +1 -1
  2. lino/api/doctest.py +14 -0
  3. lino/core/actions.py +2 -8
  4. lino/core/actors.py +5 -3
  5. lino/core/choicelists.py +8 -11
  6. lino/core/dbtables.py +7 -7
  7. lino/core/elems.py +16 -11
  8. lino/core/fields.py +67 -19
  9. lino/core/kernel.py +6 -0
  10. lino/core/layouts.py +2 -2
  11. lino/core/model.py +15 -12
  12. lino/core/plugin.py +2 -2
  13. lino/core/site.py +11 -13
  14. lino/core/store.py +5 -6
  15. lino/core/tables.py +13 -9
  16. lino/core/utils.py +4 -23
  17. lino/help_texts.py +9 -1
  18. lino/mixins/duplicable.py +9 -3
  19. lino/mixins/ref.py +9 -0
  20. lino/mixins/registrable.py +2 -2
  21. lino/mixins/sequenced.py +12 -8
  22. lino/modlib/extjs/ext_renderer.py +1 -1
  23. lino/modlib/gfks/fields.py +1 -1
  24. lino/modlib/help/models.py +2 -1
  25. lino/modlib/jinja/renderer.py +1 -0
  26. lino/modlib/linod/models.py +1 -1
  27. lino/modlib/notify/fixtures/demo2.py +7 -2
  28. lino/modlib/periods/fixtures/std.py +1 -3
  29. lino/modlib/periods/models.py +26 -5
  30. lino/modlib/publisher/fixtures/synodalworld.py +0 -2
  31. lino/modlib/weasyprint/__init__.py +1 -0
  32. lino/modlib/weasyprint/choicelists.py +2 -11
  33. lino/modlib/weasyprint/config/weasyprint/base.weasy.html +1 -1
  34. lino/utils/__init__.py +20 -0
  35. lino/utils/choosers.py +21 -15
  36. lino/utils/quantities.py +21 -3
  37. {lino-25.4.5.dist-info → lino-25.5.1.dist-info}/METADATA +1 -1
  38. {lino-25.4.5.dist-info → lino-25.5.1.dist-info}/RECORD +41 -41
  39. {lino-25.4.5.dist-info → lino-25.5.1.dist-info}/WHEEL +0 -0
  40. {lino-25.4.5.dist-info → lino-25.5.1.dist-info}/licenses/AUTHORS.rst +0 -0
  41. {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` which depends on the request.
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 `get_filter_kw` would return a dict `{'author':
564
- <PK>}` where `<PK>` is the primary key of the action request's
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
- :class:`lino_xl.lib.tickets.EntriesBySession`, where blog
570
- entries are not directly linked to a session, but in the
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 importlib import import_module
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 django.core.validators import validate_email, ValidationError, URLValidator
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 which duplicates (creates a clone of) the object it was called on."""),
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-2020 Rumma & Ko Ltd
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)
@@ -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:`ledger voucher` is registered, its associated
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 ledger voucher.
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 is None: # TODO: remove this as it is no longer used (?)
283
- self.seqno = 0
282
+ if (count := qs.count()) == 0:
283
+ self.seqno = 1
284
284
  else:
285
- n = qs.count()
286
- if n == 0:
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 "green blue red yellow".split():
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()
@@ -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
 
@@ -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")
@@ -71,6 +71,7 @@ class JinjaRenderer(HtmlRenderer):
71
71
  # ~ print 20130109, loaders
72
72
  self.jinja_env = jinja2.Environment(
73
73
  # ~ extensions=['jinja2.ext.i18n'],
74
+ extensions=['jinja2.ext.do'],
74
75
  loader=jinja2.ChoiceLoader(loaders)
75
76
  )
76
77
 
@@ -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-2018 Rumma & Ko Ltd
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
- now = datetime.datetime.combine(dd.today(), i2t(548))
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))
@@ -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(default='open')
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(default='open')
98
- year = dd.ForeignKey('periods.StoredYear', blank=True, null=True, related_name="periods")
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: return qs
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'
@@ -15,8 +15,6 @@ home_children = [
15
15
 
16
16
  def objects():
17
17
  image = rt.models.uploads.Upload.objects.first()
18
- print('='*80)
19
- print(image)
20
18
  def iterate(iterable):
21
19
  try:
22
20
  for obj in iterable:
@@ -38,6 +38,7 @@ class Plugin(ad.Plugin):
38
38
  margin = 10
39
39
  margin_left = 17
40
40
  margin_right = 10
41
+ space_before_recipient = 15
41
42
 
42
43
  def get_requirements(self, site):
43
44
  yield "imagesize"
@@ -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 copy import copy
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-2021 Rumma & Ko Ltd
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
- GFK_HACK = re.compile(
23
- r'^<a href="javascript:Lino\.(\w+\.\w+)\.detail\.run\(.*,\{ &quot;record_id&quot;: (\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
- # from lino.api import rt
37
- from lino.core import constants
38
- from lino.core import fields
39
- from lino.core.utils import getrqdata
35
+ GFK_HACK = re.compile(
36
+ r'^<a href="javascript:Lino\.(\w+\.\w+)\.detail\.run\(.*,\{ &quot;record_id&quot;: (\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 : fn.__code__.co_argcount]
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