Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More

Clients in Lino Avanti

This document describes the lino_avanti.lib.avanti plugin.

Side note: Code snippets (lines starting with >>>) in this document get tested as part of our development workflow. The following initialization snippet tells you which demo project is being used in this document.

>>> import lino
>>> lino.startup('lino_book.projects.avanti1.settings')
>>> from lino.api.doctest import *

Overview

A client is a person using our services.

The legacy file number

Dossiernummern:

  • Wenn du “ip6” eintippst, sucht Lino die letzte Dossiernummer, die mit “IP 6” beginnt und zählt +1 hinzu. Also wenn der letzte bestehende Klient “IP 6923” hat, macht Lino aus “ip6” eine “IP 6924”.

  • Du kannst auch im Schnellsuche-Feld “ip 6900” eintippen, um nach Dossiernummer zu suchen.

  • Wenn du “ip 1234” eintippst (also die Dossiernummer selber als vierstellige Zahl angibst), dann lässt Lino diese Nummer stehen.

  • Ob du “ip” oder “IP” eintippst, ist egal, Lino macht daraus immer “IP”.

  • Auch das Leerzeichen kannst du beim Eintippen sparen, das setzt Lino automatisch rein.

  • Wenn die Dossiernummer nicht mit “ip” beginnt, lässt Lino sie unverändert

>>> other = avanti.Client.objects.get(pk=116)
>>> other2 = avanti.Client.objects.get(pk=117)
>>> def update_other(ref, ref2):
...     other.ref = ref
...     other.full_clean()
...     other.save()
...     other2.ref = ref2
...     other2.full_clean()
...     other2.save()
>>> update_other(None, None) # tidy up from previous test run
>>> def test(ref):
...     obj = avanti.Client(ref=ref, name="x")
...     obj.full_clean()
...     print(obj.ref)
>>> test("ip")
IP 0001
>>> test("ip 1")
IP 1001
>>> update_other("IP 4010", "IP 5123")
>>> test("ip 4")
IP 4011
>>> test("ip")
IP 5124
>>> update_other("IP 6999", "IP 7000")
>>> test("ip6")
IP 61000
>>> update_other("IP 60999", "IP 61000")
>>> test("ip6")
IP 61001

Damit Lino die Referenzen automatisch verteilen kann, müssen alle bestehenden Dossiernummern die gleiche Länge haben. Ansonsten kann Lino durcheinander kommen. Zum Beispiel:

>>> update_other("IP 6999", "IP 61000")
>>> test("ip6")
Traceback (most recent call last):
...
django.core.exceptions.ValidationError: {'ref': ['Client with this Legacy file number already exists.']}
>>> update_other(None, None) # tidy up for the following tests

Clients

class lino_avanti.lib.avanti.Client(lino.core.model.Model)
translator_type

Which type of translator is needed with this client.

See also TranslatorTypes

professional_state

The professional situation of this client.

See also ProfessionalStates

overview

A panel with general information about this client.

client_state

The state of this client record.

This is a pointer to ClientStates and can have the following values:

>>> rt.show('clients.ClientStates')
======= ========== ============ =============
 value   name       text         Button text
------- ---------- ------------ -------------
 05      incoming   Incoming
 07      informed   Informed
 10      newcomer   Newcomer
 15      equal      Equal
 20      coached    Registered
 25      inactive   Inactive
 30      former     Ended
 40      refused    Abandoned
======= ========== ============ =============
unemployed_since

The date when this client got unemployed and stopped to have a regular work.

seeking_since

The date when this client registered as unemployed and started to look for a new job.

work_permit_suspended_until
city

The place (village or municipality) where this client lives.

See lino_xl.lib.contacts.Partner.city.

municipality

The municipality where this client lives. This is basically equal to city, except when city is a village and has a parent which is a municipality (which causes that place to be returned).

class lino_avanti.lib.avanti.ClientDetail
class lino_avanti.lib.avanti.Clients

Base class for most lists of clients.

client_state

If not empty, show only Clients whose client_state equals the specified value.

class lino_avanti.lib.avanti.AllClients(Clients)

This table is visible for Explorer who can also export it.

This table shows only a very limited set of fields because e.g. an auditor may not see all data for privacy reasons. For example the names are hidden. OTOH it includes the municipality virtual field.

>>> show_columns(avanti.AllClients, all=True)
... 
- State (client_state) : The state of this client record.
- Starting reason (starting_reason) :
- Ending reason (ending_reason) :
- Locality (city) : The locality, i.e. usually a village, city or town.
- Municipality (municipality) : The municipality where this client lives. This is basically
  equal to city, except when city is a village
  and has a parent which is a municipality (which causes that
  place to be returned).
- Country (country) :
- Zip code (zip_code) :
- Nationality (nationality) : The nationality. This is a pointer to
  countries.Country which should
  contain also entries for refugee statuses.
- Gender (gender) : The sex of this person (male or female).
- Birth country (birth_country) :
- Lives in Belgium since (in_belgium_since) : Uncomplete dates are allowed, e.g.
  "00.00.1980" means "some day in 1980",
  "00.07.1980" means "in July 1980"
  or "23.07.0000" means "on a 23th of July".
- Needs work permit (needs_work_permit) :
- Translator type (translator_type) : Which type of translator is needed with this client.
- Mother tongues (mother_tongues) :
- None (cef_level_de) :
- None (cef_level_fr) :
- None (cef_level_en) :
- Primary coach (user) : The author of this database object.
- Recurrency policy (event_policy) :
>>> rt.show(avanti.AllClients, limit=5)
... 
============ ================= =============== ============ ============== ========= ========== ============= ========== ======== =============== ======================== =================== ================= ================ =============== =============== =============== ================= =================== ============== ================
 State        Starting reason   Ending reason   Locality     Municipality   Country   Zip code   Nationality   Age        Gender   Birth country   Lives in Belgium since   Needs work permit   Translator type   Mother tongues   cef_level_de    cef_level_fr    cef_level_en    Primary coach     Recurrency policy   Erstgespräch   Bilanzgespräch
------------ ----------------- --------------- ------------ -------------- --------- ---------- ------------- ---------- -------- --------------- ------------------------ ------------------- ----------------- ---------------- --------------- --------------- --------------- ----------------- ------------------- -------------- ----------------
 Registered                                     4700 Eupen   4700 Eupen     Belgium   4700                     16 years   Male                                              No                  SETIS             Dutch            Not specified   Not specified   Not specified   nathalie          Every month         30/07/2016
 Registered                                     4700 Eupen   4700 Eupen     Belgium   4700                     20 years   Female                                            No                  Other             English          Not specified   Not specified   Not specified   Romain Raffault   Every 2 weeks
 Registered                                     4700 Eupen   4700 Eupen     Belgium   4700                     22 years   Male                                              No                  Other             French           A1+             Not specified   Not specified   Rolf Rompen       Other
 Registered                                     4700 Eupen   4700 Eupen     Belgium   4700                     24 years   Male                                              No                  Other             English          Not specified   Not specified   Not specified   Robin Rood        Every 2 months
 Registered                                     4700 Eupen   4700 Eupen     Belgium   4700                     26 years   Male                                              No                  SETIS             French           Not specified   Not specified   Not specified   nathalie          Every 3 months
============ ================= =============== ============ ============== ========= ========== ============= ========== ======== =============== ======================== =================== ================= ================ =============== =============== =============== ================= =================== ============== ================
class lino_avanti.lib.avanti.MyClients(Clients)

Shows all clients having me as primary coach. Shows all client states.

>>> rt.login('robin').show('avanti.MyClients')
... 
===================================== ============ =============== ======== ================================= ========== ================ ======= ===== ====================
 Name                                  State        National ID     Mobile   Address                           Age        e-mail address   Phone   ID    Legacy file number
------------------------------------- ------------ --------------- -------- --------------------------------- ---------- ---------------- ------- ----- --------------------
 ABDALLAH Aáish (127/robin)            Registered   920417 001-91            Bellmerin, 4700 Eupen             24 years                            127
 ABDO Aásim (138/robin)                Registered   831201 001-50            Gülcherstraße, 4700 Eupen         33 years                            138
 ABDULLAH Afááf (155/robin)            Ended        760102 002-86            4730 Raeren                       41 years                            155
 ABOUD Ahláám (166/robin)              Ended        690627 002-97            4730 Raeren                       47 years                            166
 ARENT Afánásiiá (124/robin)           Ended        891219 002-23            Bergkapellstraße, 4700 Eupen      27 years                            124
 ASTAFUROV Agáfiiá (175/robin)         Registered   820120 002-60            Aachen, Germany                   35 years                            175
 BARTOSZEWICZ Agáfokliiá (146/robin)   Ended        781018 002-02            Herbesthaler Straße, 4700 Eupen   38 years                            146
 BERENDT Antoshá (165/robin)           Ended        700602 001-93            4730 Raeren                       46 years                            165
 CONTEE Chike (131/robin)              Registered   870822 001-58            Edelstraße, 4700 Eupen            29 years                            131
 DIOP Ashánti (142/robin)              Registered   810214 002-32            Habsburgerweg, 4700 Eupen         36 years                            142
 JALLOH Diállo (158/robin)             Registered   740810 001-48            4730 Raeren                       42 years                            158
===================================== ============ =============== ======== ================================= ========== ================ ======= ===== ====================
class lino_avanti.lib.avanti.ClientsByNationality(Clients)
class lino_avanti.lib.avanti.Residence(lino.core.model.Model)
class lino_avanti.lib.avanti.EndingReason(lino.core.model.Model)
>>> rt.show('avanti.EndingReasons')
==== ======================== ========================== ========================
 ID   Designation              Designation (de)           Designation (fr)
---- ------------------------ -------------------------- ------------------------
 1    Successfully ended       Erfolgreich beendet        Successfully ended
 2    Health problems          Gesundheitsprobleme        Health problems
 3    Familiar reasons         Familiäre Gründe           Familiar reasons
 4    Missing motivation       Fehlende Motivation        Missing motivation
 5    Return to home country   Rückkehr ins Geburtsland   Return to home country
 9    Other                    Sonstige                   Autre
==== ======================== ========================== ========================
class lino_avanti.lib.avanti.Category(BabelDesignated)
>>> rt.show('avanti.Categories')
==== =============================== =============================== ===============================
 ID   Designation                     Designation (de)                Designation (fr)
---- ------------------------------- ------------------------------- -------------------------------
 1    Language course                 Sprachkurs                      Language course
 2    Integration course              Integrationskurs                Integration course
 3    Language & integration course   Language & integration course   Language & integration course
 4    External course                 External course                 External course
 5    Justified interruption          Begründete Unterbrechung        Justified interruption
 6    Successfully terminated         Erfolgreich beendet             Successfully terminated
==== =============================== =============================== ===============================
class lino_avanti.lib.avanti.TranslatorTypes

List of choices for the Client.translator_type field of a client.

>>> rt.show(rt.models.avanti.TranslatorTypes, language="de")
====== ====== ==========
 Wert   name   Text
------ ------ ----------
 10            SETIS
 20            Sonstige
 30            Privat
====== ====== ==========
class lino_avanti.lib.avanti.ProfessionalStates

List of choices for the Client.professional_state field of a client.

>>> rt.show(rt.models.avanti.ProfessionalStates, language="de")
... 
====== ====== ================================
 Wert   name   Text
------ ------ --------------------------------
 100           Student
 200           Arbeitslos
 300           Eingeschrieben beim Arbeitsamt
 400           Angestellt
 500           Selbstständig
 600           Pensioniert
 700           Arbeitsunfähig
====== ====== ================================
>>> rt.show(checkdata.Checkers, language="en")
... 
================================= =============================================
 value                             text
--------------------------------- ---------------------------------------------
 beid.SSINChecker                  Check for invalid SSINs
 cal.ConflictingEventsChecker      Check for conflicting calendar entries
 cal.EventGuestChecker             Entries without participants
 cal.LongEntryChecker              Too long-lasting calendar entries
 cal.ObsoleteEventTypeChecker      Obsolete generated calendar entries
 comments.CommentChecker           Check for missing owner in reply to comment
 countries.PlaceChecker            Check data of geographical places
 dupable.DupableChecker            Check for missing phonetic words
 dupable.SimilarObjectsChecker     Check for similar objects
 linod.SystemTaskChecker                 Check for missing system tasks
 memo.PreviewableChecker           Check for previewables needing update
 printing.CachedPrintableChecker   Check for missing target files
 system.BleachChecker              Find unbleached html content
 uploads.UploadChecker             Check metadata of upload files
 uploads.UploadsFolderChecker      Find orphaned files in uploads folder
================================= =============================================

Career

Language knowledges

Avanti adds an entry date to the language knowledge table of a client. There can be multiple entries per language and client. Because we want to report whether knowledge changed after attending a course.

Some example cases:

>>> client = rt.models.avanti.Client.objects.get(pk=120)
>>> rt.show('cv.LanguageKnowledgesByPerson', client, nosummary=True)
... 
========== =============== ============ ============ =========== ============= ============
 Language   Mother tongue   Spoken       Written      CEF level   Certificate   Entry date
---------- --------------- ------------ ------------ ----------- ------------- ------------
 Dutch      No              a bit        moderate     A2+         No            05/02/2017
 Dutch      No              moderate     quite well   A2          No            12/01/2016
 German     No              quite well   very well    A1+         No            12/01/2016
 French     Yes                                                   No            12/01/2016
========== =============== ============ ============ =========== ============= ============
>>> client = rt.models.avanti.Client.objects.get(pk=121)
>>> rt.show('cv.LanguageKnowledgesByPerson', client, nosummary=True)
... 
========== =============== ======== ========= =========== ============= ============
 Language   Mother tongue   Spoken   Written   CEF level   Certificate   Entry date
---------- --------------- -------- --------- ----------- ------------- ------------
 Estonian   Yes                                            No            12/01/2016
========== =============== ======== ========= =========== ============= ============
>>> client = rt.models.avanti.Client.objects.get(pk=122)
>>> rt.show('cv.LanguageKnowledgesByPerson', client, nosummary=True, language="de")
... 
============= =============== ============== ============== =============== ============ =================
 Sprache       Muttersprache   Wort           Schrift        CEF-Kategorie   Zertifikat   Erfassungsdatum
------------- --------------- -------------- -------------- --------------- ------------ -----------------
 Deutsch       Nein            gar nicht      ein bisschen   A1              Nein         05.02.17
 Deutsch       Nein            ein bisschen   mittelmäßig    A0              Nein         12.01.16
 Französisch   Ja                                                            Nein         12.01.16
============= =============== ============== ============== =============== ============ =================

The end user usually sees the summary of language knowledges , which shows the CEF level of the languages defined in lino.core.site.Site.languages, and only the most recent CEF level. For above client the CEF level for German is A1 (not A0):

>>> rt.show('cv.LanguageKnowledgesByPerson', client, language="de")
... 
en: Ohne Angabe
de: A1
fr: Ohne Angabe
Muttersprachen: Französisch

Creating a new client

>>> ses = rt.login("romain")
>>> url = '/api/avanti/MyClients/-99999?an=insert&fmt=json'
>>> test_client.force_login(ses.user)
>>> res = test_client.get(url)
>>> res.status_code
200
>>> d = AttrDict(json.loads(res.content))
>>> sorted(d.keys())
... 
['data', 'phantom', 'title']
>>> d.phantom
True
>>> print(d.title)
Insérer Bénéficiaire

The dialog window has 6 data fields:

>>> sorted(d.data.keys())  
['disabled_fields', 'email', 'first_name', 'gender', 'genderHidden', 'last_name']
>>> fld = avanti.Clients.parameters['observed_event']
>>> rt.show(fld.choicelist, language="en")
No data to display

Miscellaneous

Until 20200818 the help_text of the municipality field wasn’t set at all, and the help text of Partner.city talked about a client because it had been overwritten by the help text of lino_avanti.lib.contacts.Person.city.

On 20210709 (after moving help texts to separate plugin lino.modlib.help) there was another subtle problem here.

Compare (a) the specs (i.e. the target of the links) and (b) the help texts of the following fields:

The lino_xl.lib.countries.CountryCity.municipality field is defined on the lino_xl.lib.countries.CountryCity model mixin. It is documented on countries : Countries and cities, which causes the help_text_extractor to extract its help_text:

The locality, i.e. usually a village, city or town.

This help_text is inherited by all models that use this model mixin: Person, Partner, Company, Client. But Client overrides it again to be more specific.

>>> print(contacts.Person._meta.get_field('municipality').help_text)
The municipality, i.e. either the city or a parent of it.
>>> print(contacts.Person._meta.get_field('city').help_text)
The locality, i.e. usually a village, city or town.
>>> print(contacts.Person._meta.get_field('city').help_text)
The locality, i.e. usually a village, city or town.
>>> print(avanti.Client._meta.get_field('municipality').help_text)
The municipality where this client lives. This is basically equal to city, except when city is a village and has a parent which is a municipality (which causes that place to be returned).

Don’t read

>>> obj = avanti.Client.objects.get(pk=167)
>>> rt.login("robin").show("changes.ChangesByMaster", obj)
Aucun enregistrement

The following (specifying a wrong mt) caused a server traceback until 20230620

>>> url = "/api/changes/ChangesByMaster?dm=grid&fmt=json&limit=15&mk=167&mt=75&start=0&ul=en&wt=d"
>>> res = test_client.get(url)
>>> d = json.loads(res.content.decode())
>>> print(d['title'])
Changes of MissingRow(Line matching query does not exist. (pk=167))

#5751 ObjectDoesNotExist: Invalid primary key 4968 for avanti.Clients

Before 20240910, the following request was returning {'data': 'Oops, Invalid primary key 115 for avanti.Clients'} because client 115 has client_state “former” and therefore is not visible in the default avanti.Clients table. This was #5751. Until 20241004 Lino didn’t check at all whether the table (avanti.Clients in this case gives access to a particular row). This was #5759 (Anonymous can GET private comments).

>>> ses = rt.login("robin")
>>> test_client.force_login(ses.user)
>>> pk = 115  # 166
>>> cli = avanti.Client.objects.get(pk=pk)
>>> cli.client_state
<clients.ClientStates.former:30>
>>> url = f"/api/avanti/Clients/{pk}?fmt=json"
>>> res = test_client.get(url)
Not Found: /api/avanti/Clients/115
>>> res.status_code
404

This 404 Not Found response is because indeed client 115 does not exist in the avanti.Clients table when using its default parameters (i.e. show only coached clients). That’s why the client must also provide the parameter values when asking for a single database row. Another reason for the parameter values is navigation: Lino does not only return data of the row but also navinfo with pointers to next and previous rows.

>>> url += "&fmt=json&pv=&pv=&pv=&pv=&pv=&pv=&pv=&pv=&pv=&pv=&pv="
>>> res = test_client.get(url)
>>> d = json.loads(res.content.decode())
>>> d['data']['first_name']
'Aábid'
>>> d['data']['cal.GuestsByPartner']  
'<div><table cellspacing="3px" bgcolor="#eeeeee" width="100%"
name="cal.GuestsByPartner.grid">... title="Insert a new Presence." class="pi
pi-plus-circle" /></p></div>'

The same is true for delayed values:

>>> url = f"/values/avanti/Clients/{pk}/cal.GuestsByPartner"
>>> res = test_client.get(url)
Bad Request: /values/avanti/Clients/115/cal.GuestsByPartner
>>> url += "?pv=&pv=&pv=&pv=&pv=&pv=&pv=&pv=&pv=&pv=&pv="
>>> res = test_client.get(url)
>>> d = json.loads(res.content.decode())
>>> d  
{'data': '<div class="htmlText"><ul><li>February 2017: ... <a href="..."
title="Insert a new Presence." class="pi pi-plus-circle"></a></p></div>'}