Quintilien : aspects techniques
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
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)