Quintilien

Quis, quid, ubi, quibus auxiliis, cur, quomodo, quando

Quintilien : aspects techniques

Page précédente

Répertoires et fichiers

Les répertoires et les fichiers utilisés par Quintilien sont repris en détail dans le descriptif (pdf) que vous pouvez télécharger par ailleurs.

En résumé

  • le répertoire Quintilien est le répertoire dans lequel se trouvent les données (".db") : pensez à vos backups !
  • par défaut (et sauf installation personnalisée), le répertoire "c:\program files (x86)\quintilien" contient le "quintilien.exe" et le "quintilien.ini"
  • le fichier ".ini" peut être ouvert et modifié par le bloc-note (Notepad). Si ce fichier se trouve dans c:\program files, il est nécessaire d'avoir des droits d'administration pour le modifier.

Structure de la base de données

Par convention, dans toutes les tables :

  • _id est un identifiant unique de l'enregistrement (numérique entier, indexé)
  • _ref est la référence (du client, du dossier, etc), en 12 caractères maximum, majuscules et/ou chiffres, sans espaces,
  • _name ou _descr nom ou description, en 40 caractères maximum
  • _ext est une zone non utilisée par Quintilien, destinée à être prise en compte dans le cadre d'extensions éventuelles du logiciel
  • _sleep est un champ numérique (entier) qui contient la valeur 0 (par défaut) ou 1 (si l'enregistrement est "en sommeil"), sauf dans le cas de l'utilisateur spécial "admin', pour lequel sleep est à la valeur -1 (cf plus loin)

Les champs "_ext " pourraient par exemple servir

  • à définir des catégories de client
  • à contenir la description des dossiers
  • à incorporer des catégories de tarifs
  • à mémoriser un budget ou un prix de revient standard

Paramètres

Toujours activer le jeu de caractères UTF-8, ainsi que les "foreign keys" (tables liées)

  • PRAGMA encoding = 'UTF-8'
  • PRAGMA foreign_keys = ON

La table des paramètres.

CREATE TABLE "t_param" (
'param_id' INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
'param_ref' TEXT NOT NULL UNIQUE,
'param_1' TEXT DEFAULT '',
'param_2' TEXT DEFAULT '',
'param_3' TEXT DEFAULT '',
'param_4' TEXT DEFAULT '' )

Le premier enregistrement de cette table - identifié par la référence "Version" - contient le numéro de version de la base de données ainsi que le code de la langue à utiliser par défaut.

Les autres enregistrements contiennent la traduction FR/NL/UK des zones affichées sur les différents écrans (une quatrième langue peut être définie, via le champ param_4, si on le souhaite).

La table des clients

CREATE TABLE "t_customer" (
'cust_id' INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
'cust_ref' TEXT NOT NULL UNIQUE,
'cust_name' TEXT DEFAULT '',
'cust_ext' TEXT DEFAULT '',
'cust_sleep' INTEGER DEFAULT 0 )

La table des dossiers (ou des commandes)

CREATE TABLE "t_order" (
'ord_id' INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
'ord_ref' TEXT NOT NULL UNIQUE,
'ord_fk_cust_ref' TEXT,
'ord_descr' TEXT DEFAULT '',
'ord_ext' TEXT DEFAULT '',
'ord_sleep' INTEGER DEFAULT 0,
FOREIGN KEY('ord_fk_cust_ref') REFERENCES 't_customer'('cust_ref') )

Liens entre dossiers et clients.

Le lien est assuré par une clé étrangère ("foreign key") qui contient la référence du client.

La table des prestataires (utilisateurs)

CREATE TABLE "t_who" (
'who_id' INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
'who_ref' TEXT NOT NULL UNIQUE,
'who_name' TEXT DEFAULT '',
'who_ext' TEXT DEFAULT '',
'who_price' REAL DEFAULT 0.0,
'who_pswd' TEXT DEFAULT '',
'who_admin' INTEGER DEFAULT 0,
'who_sleep' INTEGER NOT NULL DEFAULT 0 )

Cette table contient toujours un enregistrement qui définit l'utilisateur "admin", avec un code "sommeil" à -1

INSERT INTO t_who (who_ref, who_name, who_pswd, who_admin, who_sleep) \
VALUES ("ADMIN", "Admin", "", 1, -1)

La table des unités

CREATE TABLE "t_unit" (
'unit_id' INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
'unit_ref' TEXT NOT NULL UNIQUE,
'unit_descr' TEXT DEFAULT '',
'unit_sleep' INTEGER DEFAULT 0 )

La table des codes de prestations

CREATE TABLE "t_what" (
'what_id' INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
'what_ref' TEXT NOT NULL UNIQUE,
'what_descr' TEXT DEFAULT '',
'what_fk_unit_ref' TEXT,
'what_ext' TEXT DEFAULT '',
'what_price' REAL DEFAULT 0.0,
'what_sleep' INTEGER DEFAULT 0,
FOREIGN KEY('what_fk_unit_ref') REFERENCES 't_unit'('unit_ref') )

Liens entre prestations et unités de mesure

Le lien est assuré par une clé étrangère ("foreign key") qui contient la référence de l'unité.

La table "time-sheet"

CREATE TABLE "t_who_when_what" (
'w_id' INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
'w_fk_who_ref' TEXT,
'w_when' TEXT NOT NULL,
'w_fk_ord_ref' TEXT,
'w_fk_what_ref' TEXT,
'w_descr' TEXT,
'w_qty' REAL DEFAULT 0.0,
'w_sleep' INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY('w_fk_who_ref') REFERENCES 't_who'('who_ref'),
FOREIGN KEY('w_fk_ord_ref') REFERENCES 't_order'('ord_ref'),
FOREIGN KEY('w_fk_what_ref') REFERENCES 't_what'('what_ref') )

Liens entre le time-sheet et les autres tables

Trois foreign-keys assurent l'intégrité du time-sheet par rapport au reste des données :

  • la référence de l'utilisateur (prestataire)
  • la référence du dossier (elle-même liée à un client)
  • la référence de la prestation (job), elle-même liée à une unité de mesure (le plus souvent des heures ou des jours)

Remarques

  • Le format utilisé pour les dates (zone 'w_when' text not null) est YYYY-MM-DD
  • La zone w_sleep n'est jamais modifiée par Quintilien (dans sa version actuelle).

Par contre, la valeur de w_sleep est prise en compte lors de l'affichage du Timesheet sur l'écran principal : si la zone ne contient pas valeur zéro (c-à-d sa valeur par défaut), la ligne qui correspond à l'enregistrement n'est plus affichée sur l'écran de saisie (et n'est donc plus modifiable).

Une application externe à Quintilien qui souhaiterait verrouiller la modification d'une ligne pourrait utiliser cette fonctionnalité (pour interdire la modification d'un encodage une fois une prestation facturée, par exemple).

La valeur de la zone n'affecte en rien le fonctionnement du reste de l'application (les listings, par exemples, tiennent compte de tous les enregistrements de la table t_who_when_what quelle que soit la valeur de w_sleep).

Critères de validité d'une référence

Quintilien convertit automatiquement les références suivant les critères suivants

  • 12 positions maximum
  • caractères alphabétiques ou numériques
  • tous les caractères alphabétiques sont en majuscules
  • pas d'espaces
  • pas de caractères spéciaux (sauf le tiret '-' et le souligné '_')

Ces critères sont applicables à toutes les références ('_ref') des tables gérées par Quintilien

Exemple en Python :

def validate_ref(ref):  # test a reference and convert it to 12 uppercase charact.
    ref = ref.strip() # remove spaces in front and behind
    trs = str.maketrans("âäàéèêëîïôöùûüçÿ ²&'(§!)^$µ,;:=<>³°¨*%£?./+|@#{[^{}[]`´",\
    "aaaeeeeiioouuucy_$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$")
    ref = ref.translate(trs)    # translate 'exotics' charact.
    ref = ref.upper()   # convert in uppercase
    ref = ref.replace('$','') # remove exotics charact.
    ref = ref.replace('"','') # remove quotes
    if len(ref)>12: # use 12 charact. max
            ref = ref[0:12] 
    return ref

 

Retourner en début de page

 

 

Le coin des bidouilleurs

Exemples d'applications externes

(Python 3 doit être installé)

Exemple 1 :

  • lire le fichier '.ini'
  • afficher l'emplacement de la base de données
  • afficher la version de la base de données et la langue à utiliser par défaut.
  • afficher les références des utilisateurs, des codes de prestation et des dossiers, ainsi - que le nombre d'enregistrements dans le time-sheet des données de démonstration,
  • générer aléatoirement 10.000 enregistrements supplémentaires dans le time-sheet

#!/usr/bin/env python
#-*- coding: utf-8 -*-
#from tkinter import * 
#from tkinter.ttk import Combobox
import sqlite3
#import time
from datetime import *
import os
#import sys
#import hashlib
import random

def validate_ref(ref):
    """ tester une référence et la transformer en 12 majuscules
    """
    ref = ref.strip()
    trs = str.maketrans("âäàéèêëîïôöùûüçÿ ²&'(§!)^$µ,;:=<>³°¨*%£?./+|@#{[^{}[]`´", \
                        "aaaeeeeiioouuucy_$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$")
    ref = ref.translate(trs)
    ref = ref.upper()
    ref = ref.replace('$','')
    ref = ref.replace('"','')
    if len(ref)>12:
        ref = ref[0:12] 
    return ref


def read_ini():
    dbpath = ''
    dbname = ''
    dbdemo = ''
    tmp_read = ''
    try:
        with open('quintilien.ini', 'r') as tmp_file:
            tmp_read = tmp_file.read()
    except:
        pass
    tmp_read = tmp_read.split("\n")
    for tmp_line in tmp_read:
        tmp_line = tmp_line.translate(str.maketrans("\\", "/"))
        tmp_line = tmp_line.translate(str.maketrans('"', ' ')) 
        tmp_line = tmp_line.translate(str.maketrans("'", " ")) 
        tmp_line = tmp_line.strip()
        param = tmp_line.split("=")
        if len(param) == 2:
            param[0] = ( param[0].lower() ).strip()
            param[1] = param[1].strip()
            if (param[1][len(param[1])-1:len(param[1])]) != '/':
                param[1] = param[1] + '/'
            if param[0][0:2] == 'db':
                dbpath = param[1].strip()
        if dbpath != '':
            if os.path.exists(dbpath+'quintilien_demo.sqlite.db'):
                dbdemo = dbpath+'quintilien_demo.sqlite.db'
            if os.path.exists(dbpath+'quintilien.sqlite.db'):
                dbname = dbpath+'quintilien.sqlite.db'
    return dbdemo, dbname


def db_param(dbname):
    dbversion = ''
    lang    = list()
    lg = 0     
    try:
        db = sqlite3.connect(dbname)
        cur = db.cursor()
        cur.execute('''SELECT * FROM t_param WHERE param_ref = "Version"''')
        rows = cur.fetchall()
        dbversion = rows[0][2] # db version
        lg     = rows[0][4] # language (0-3)
        if lg.isdigit():
            lg = int(lg)
        else:
            lg = 0
        if lg > 4:
            lg = 0         
        cur.execute('''SELECT * FROM t_param WHERE param_ref = "Language"''')
        rows = cur.fetchall()
        lang    = rows[0][2:] # languages list
        db.close()
    except:
        pass
    return dbversion, lang, lg

def display(dbversion, lang, lg):
    if dbversion != '':
        print (' - version : ',dbversion)
        print (' - language : ',lg)
        print (' - available : ',lang)
        if len(lang) > 0:
            print (' - selected : ',lang[lg])


def read_some_tables(dbname):

    db = sqlite3.connect(dbname)
    cur = db.cursor() 
    cur.execute('''SELECT * FROM t_who WHERE t_who.who_sleep < 1 ''')
    rows = cur.fetchall()
    users = [row[1] for row in rows]
    cur.execute('''SELECT * FROM t_what WHERE t_what.what_sleep < 1 ''')
    rows = cur.fetchall()
    jobs = [row[1] for row in rows]
    cur.execute('''SELECT * FROM t_order WHERE t_order.ord_sleep < 1 ''')
    rows = cur.fetchall()
    orders = [row[1] for row in rows]
    cur.execute('''SELECT * FROM t_who_when_what WHERE w_sleep < 1 ''')
    rows = cur.fetchall()
    timesheet = [row[1] for row in rows]
    db.close()
    return users, jobs, orders, timesheet


def add_records_in_demo(dbname, nb):

    db = sqlite3.connect(dbname)
    cur = db.cursor() 

    blah = ('Lorem ipsum dolor sit amet, consectetur adipiscing elit, \
            sed do eiusmod tempor incididunt ut labore et dolore magna aliqua \
            Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris \
            nisi ut aliquip ex ea commodo consequat \
            Duis aute irure dolor in reprehenderit in voluptate velit esse \
            cillum dolore eu fugiat nulla pariatur \
            Excepteur sint occaecat cupidatat non proident, sunt in culpa \
            qui officia deserunt mollit anim id est laborum').split()

    y = int ( str ( datetime.today() )[0:4] )     # current year
    jan01 = datetime( y-1, 1, 1 )                 # Jan 01 previous year
    elapsed = ( datetime.today() - jan01 ).days    # elapsed days 

    for i in range (nb):                            # nbr of records to create
        d=random.randint(0,elapsed)                 # random date starting Jan01
        date = (str ( jan01 + timedelta(days=d) ) )[0:10]
        txt = ''                                    # random text
        for j in range (5):
            txt = txt + ' ' + blah[ random.randint(0, len(blah)-1) ]
        txt = txt.strip()[0:40]
        qty = float ( ( random.randrange(25, 1025, 25) ) / 100 ) # random qty

        cur.execute('''INSERT INTO "t_who_when_what" \
            (w_fk_who_ref, w_when, w_fk_ord_ref, w_fk_what_ref, w_descr, w_qty)
            VALUES (?, ?, ?, ?, ?, ?)''', \
            (random.choice(users[1:]), date, random.choice(orders), \
            random.choice(jobs), txt, qty))        # users[1:] to avoid using admin

    db.commit()
    db.close()


# ------------------------------------------------------------------ main progr
dbdemo, dbname = read_ini()

print ('db = ',dbname)
dbversion, lang, lg = db_param(dbname)
display(dbversion, lang, lg)

print ('demo = ',dbdemo)
dbversion, lang, lg = db_param(dbdemo)
display(dbversion, lang, lg)
if dbdemo:
    users, jobs, orders, timesheet = read_some_tables(dbdemo)
    print (' - users : ',users)
    print (' - jobs : ',jobs)
    print (' - orders : ',orders)
    print (' - records in timesheet : ',len(timesheet))

if dbdemo:
    nbr = 10000 # number of records to add
    add_records_in_demo(dbdemo, nbr)
    print (nbr, ' records added in timesheet')

Exemple 2 :

  • vérifier si un utilisateur existe
  • vérifier si le mot de passe introduit est correct
  • vérifier s'il est admin

#!/usr/bin/env python
#-*- coding: utf-8 -*-
#from tkinter import * 
#from tkinter.ttk import Combobox
import sqlite3
#import time
from datetime import *
import os
import sys
import hashlib
#import random

# Quintilien Check user / password in db demo

def validate_ref(ref):
    """ tester une référence et la transformer en 12 majuscules
    """
    ref = ref.strip()
    trs = str.maketrans("âäàéèêëîïôöùûüçÿ ²&'(§!)^$µ,;:=<>³°¨*%£?./+|@#{[^{}[]`´", \
                        "aaaeeeeiioouuucy_$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$")
    ref = ref.translate(trs)
    ref = ref.upper()
    ref = ref.replace('$','')
    ref = ref.replace('"','')
    if len(ref)>12:
        ref = ref[0:12] 
    return ref


def read_ini():
    dbpath = ''
    dbname = ''
    dbdemo = ''
    tmp_read = ''
    try:
        with open('quintilien.ini', 'r') as tmp_file:
            tmp_read = tmp_file.read()
    except:
        pass
    tmp_read = tmp_read.split("\n")
    for tmp_line in tmp_read:
        tmp_line = tmp_line.translate(str.maketrans("\\", "/"))
        tmp_line = tmp_line.translate(str.maketrans('"', ' ')) 
        tmp_line = tmp_line.translate(str.maketrans("'", " ")) 
        tmp_line = tmp_line.strip()
        param = tmp_line.split("=")
        if len(param) == 2:
            param[0] = ( param[0].lower() ).strip()
            param[1] = param[1].strip()
            if (param[1][len(param[1])-1:len(param[1])]) != '/':
                param[1] = param[1] + '/'
            if param[0][0:2] == 'db':
                dbpath = param[1].strip()
        if dbpath != '':
            if os.path.exists(dbpath+'quintilien_demo.sqlite.db'):
                dbdemo = dbpath+'quintilien_demo.sqlite.db'
            if os.path.exists(dbpath+'quintilien.sqlite.db'):
                dbname = dbpath+'quintilien.sqlite.db'
    return dbdemo, dbname


def check_user(dbname, user_in, psw_in):
    user_ok = False
    name    = ''
    psw_ok = False
    admin = False
    db = sqlite3.connect(dbname)
    cur = db.cursor()
    cur.execute('''SELECT * FROM t_who \
        WHERE t_who.who_ref == ? AND t_who.who_sleep < 1 ''',\
        (validate_ref(user_in),))
    rec = cur.fetchall()
    db.close()
    if len(rec) != 0:
        rec_id, ref, name, ext, price, psw_read, admin_read, sleep = rec[0]
        user_ok = True
        if admin_read == 1:
            admin = True
        if psw_in.strip() == '' and psw_read.strip() == '':
            psw_ok = True
        if psw_in.strip() != '' and psw_read.strip() == \
            hashlib.sha1(psw_in.strip().encode()).hexdigest():
            psw_ok = True
    return user_ok, name, psw_ok, admin

# ------------------------------------------------------------------ main progr
dbdemo, dbname = read_ini()
if not dbdemo:
    print ('Quintilien demo not found')
else:
    print (dbdemo)
    user = '...'
    while user:
        user = input('User (or to quit) ? : ')
        if user:
            psw = input('Password ? : ')
            user_ok, name, psw_ok, admin = check_user(dbdemo, user, psw)
            print ('user exists : ', user_ok)
            print ('name        : ', name)
            print ('password    : ', psw_ok)
            print ('admin     : ', admin)


Retourner en début de page
Table des matières ↑↑