Report report_paghe.py
Descrizione e studio del sorgente python che produce i report delle presenze
È riduttivo considerare il file dati_paghe.py solo per l'aspetto della generazione del report con la stampa periodica dei dati delle presenze mensili. In realtà è un vero e proprio programma che permette il controllo e la gestione di quei dati.
Questo documento analizza il file Python dati_paghe.py il quale report in formato PDF utilizzando la libreria ReportLab basandosi su dati provenienti da un database PostgreSQL.
Nota
Non abbiamo trovato esempi di report complessi e/o generati con dati di un DB PosgreSQL con la libreria ReportLab se non quelli della pur completa documentazione. Pensando che a qualcuno possa interessare un esempio di questo tipo abbiamo deciso di pubblicare il codice e questo documento.
I fogli delle presenze mensili dei Dipendenti
I dati che concorrono a comporre un foglio mensile delle presenze dei dipendenti provengono da diverse tabelle di un database: i dati statici dei dipendenti, contratti, orari di lavoro oltre ai dati relativi alle ore lavorate, alle assenze e al calendario delle festività. Nel nostro caso tali dati già vengono inseriti per la normale programmazione dei lavori in un database PostgreSQL, indipendentemente dalle procedure relative alle paghe. Il report dati_paghe.py semplifica notevolmente la nostra procedura mensile per la gestione dei dati delle retribuzioni e per comunicarli all'ufficio paghe.
I tipi di contratto, di rapporto di lavoro e di orario settimanale [1] di ciascun dipendente determinano un ipotetico calendario individuale che è il riferimento per la corretta suddivisione e comunicazione delle presenze o assenze giornaliere in base alle norme vigenti.
Le presenze (ore lavorate) devono essere suddivise, sulla base appunto dell'orario ipotetico di riferimento, in ore ordinarie e ore straordinarie. Il nostro caso (servizi) è un esempio relativamente complesso in quanto vengono applicati vari tipi di orario (full-part-time) e i Dipendenti spesso non fanno le ore giornaliere previste dal contratto di riferimento, accumulando o usufruendo di conseguenza delle ore di flessibilità.
Una assenza può avere base giornaliera (che comporta una assenza in ore pari a quelle del calendario ipotetico di quel giorno per quel Dipendente), piuttosto che oraria (quella che ha normalmente una durata inferiore all'orario di riferimento). L'assenza giornaliera considera completato l'orario della giornata mentre in corrispondenza di una assenza oraria vi sono abitualmente anche delle ore lavorate ad esaurire l'orario previsto dal contratto. Il nostro ufficio paghe di riferimento adotta, per la comunicazione relativa alle assenze, la loro classificazione in circa 12 tipologie di assenza oraria e 14 tipologie di assenza giornaliera.
Come già detto, il nostro contratto prevede anche la gestione della flessibilità che consiste nella gestione di un ulteriore serbatoio di ore (con una gestione delle scadenze di liquidazione su base semestrale). Nel foglio mensile delle presenze devono essere comunicate le eventuali variazioni e/o eventuali liquidazioni. Il tutto viene notificato in busta paga analogamente a ferie, permessi ecc..
Può essere necessario erogare una indennità al Dipendente a vario titolo. Pure quella va debitamente comunicata tramite il foglio presenze per l'inserimento in busta paga. Nel nostro caso abbiamo la possibilità di gestire una indennità giornaliera sia su base oraria che forfetaria. Nonostante la sua base giornaliera, questo dato, analogamente alla flessibilità, viene raggruppato e comunicato con un unico valore mensile.
| [1] | Oltre ad alcune altre variabili come appunto il luogo di lavoro (sede dell'ufficio di collocamento di riferimento), che determina le date delle festività locali. |
Controllo delle presenze e delle comunicazioni all'ufficio paghe
Come detto il file dati_paghe.py consente, oltre alla generazione ed alla stampa del foglio presenze, la gestione ed il controllo delle presenze stesse. Nella sua utilizzazione estesa (opzione -c, --controllo) esso genera infatti un documento con gli stessi dati che servono per la comunicazione all'ufficio paghe più altre righe per il controllo della completezza e della congruità dei dati.
Organizzazione del file sorgente
Generalità
Il file report_paghe.py può generare al volo due tipi di documento con i dati delle presenze e delle assenze dei Dipendenti in un dato periodo. I dati provengono da un sottostante dataset e possono essere filtrati e trattati differentemente in base alle opzioni che vengono passate a linea di comando.
Nel sorgente sono stabilite oltre che l'aspetto (layout) finale della stampa, la modalità di accesso e di acquisizione dei dati, la loro manipolazione e organizzazione .
Un primo tipo di report (l'opzione di default) consente la generazione di un report di controllo della distribuzione delle ore di presenza in ordinarie e straordinarie, del corretto inserimento di assenze, ore di flessibilità e di eventuali indennità giornaliere ecc. dei Dipendenti [2].
Una sottotabella riporta i dati dei residui di ferie, permessi ecc. in modo da consentire una loro rapida gestione e programmazione.
Il secondo tipo di report (opzione -c) permette la stampa di una parte dei dati del report precedente e precisamente quella parte necessaria alla comunicazione periodica delle presenze e delle assenze dei Dipendenti all'ufficio paghe per la compilazione dei cedolini paga mensili.
Le istruzioni a riga di comando (vedi Uso e opzioni) comprendono la definizione del periodo e il tipo di report da generare.
Il documento PDF finale è un complesso insieme di elementi, campi, tabelle e sottotabelle, dei quali molti, tipicamente, si ripetono per ciascun Dipendente.
Il file dati_paghe.py contiene tutta la logica [3] che implementa il report ed è un completo esempio di uso delle potenzialità della libreria ReportLab. In questo documento lo analizziamo per capirne i dettagli.
| [2] | NB: La stampa di questo report con l'opzione -D DIPENDENTE (cioé i dati di un solo dipendente) può essere utilizzata anche come comunicazione di verifica e controllo ai singoli collaboratori da consegnare contemporaneamente al foglio paga. |
| [3] | La connessione ai dati e la loro struttura logica è evidentemente locale e forse poco significativa al di fuori di Arti e Mestieri.. |
Origine dei dati locali
Il nostro report data_paghe.py si connette al database PostgreSQL locale GAMPG (default modificabile con l'opzione --db=DB). Vengono eseguite due query SQL (vedi Importazione dei dati dal database) che utilizzano una funzione ed alcune tabelle del database.
La funzione diario_dipendente() può restituire gran parte dei dati giornalieri che ci interessano e noi la invochiamo nella prima delle due query passandole i parametri opportuni:
diario_dipendente(%(iddip)s, %(dal)s, %(al)s, %(live)s)
dove, in base alle opzioni che diamo tramite la riga di comando quando lanciamo il report, al parametro iddip arriva l'elenco degli ID dei dipendenti filtrati dalle opzioni -D DIPENDENTE o -I INPS (default=NULL ovvero tutti i dipendenti), ai parametri dal e al i valori delle date introdotte con le corrispettive opzioni o quelle calcolate sulla base delle opzioni analoghe (-m MESE), e al parametro live, per default a True, che può essere diversamente impostato tramite l'opzione -n --no-live.
Se non si imposta quest'ultima opzione (-n --no-live), i dati vengono calcolati al volo dalla funzione sulla scorta dei dati vivi delle tabelle SchedeDipendenti e AssenzeDipendenti (oltre che sui dati relativi al contratto e orario del Dipendente).
Se invece si imposta l'opzione -n --no-live vengono presi i dati storici registrati nella tabella DiarioDipendenti, ovvero i dati ufficiali che sono stati (già) comunicati all'ufficio paghe (con gli eventuali errori compresi).
Con analoghi criteri di filtro sui dipendenti e sul periodo la seconda query estrae i dati anagrafici ed i dati mensili del Dipendente necessari alla generazione del report.
Le tabelle e le funzioni che alimentano il dataset con i dati necessari sono quindi:
| report dai dati dinamici | report dai dati statici |
|---|---|
| diario_dipendente() | diario_dipendente() |
| calendario_dipendente() | DiarioDipendenti |
| Dipendenti | Dipendenti |
| RapportiDiLavoro | RapportiDiLavoro |
| AssenzeDipendenti | TipologieContratto |
| TipologieAssenza | Retribuzioni |
| RapportiniDiGiornata | |
| SchedeDipendenti | |
| TipologieContratto | |
| Retribuzioni |
Linguaggio e struttura
Il sorgente è scritto in Python e genera al volo il documento finale stampabile nel formato PDF.
Il file dati_paghe.py non ha una sua particolare struttura se non la consueta successione degli elementi del linguaggio Python. La suddivisione che faremo in seguito ha solo funzione di riferimento nell'analisi del report.
Uso e opzioni
Il report va lanciato con un comando del tipo:
python dati_paghe.py -m 10 --anno=2004 -D 102 -c -o controllo_102_ottobre.pdf
che genera (al volo, dai dati freschi) il file controllo_102_ottobre.pdf con tutti i dati relativi al mese di ottobre (-m 10) dell'anno 2004 per il solo Dipendente con ID 102.
Si può ottenere l'elenco completo delle opzioni disponibili con il loro significato inserendo a terminale il comando:
python dati_paghe.py -h
che genera il seguente output:
usage: dati_paghe.py [options]
options:
-h, --help show this help message and exit
-D DIPENDENTE, --dipendente=DIPENDENTE
ID del dipendente, per stamparne solo uno
-I INPS, --inps=INPS Posizione INPS, per stampare solo quelli
-d DATA, --dal=DATA Data inizio periodo
-a DATA, --al=DATA Data fine periodo
-m MESE, --mese=MESE Specifica il periodo con il mese di competenza
--anno=ANNO Specifica l'anno, se diverso da quello corrente
-c, --controllo Specifica se stampare le righe di controllo
-o FILE, --output=FILE
Nome del file PDF di output
--db=DB Specifica la string di connessione al GAMPG
-n, --no-live Prendi i dati storici anziché calcolarli dai dati
correnti
i valori di default e i loro significati sono:
-D DIPENDENTE --> NULL (vengono stampati i dati di tutti i dipendenti)
-I INPS --> NULL (vengono stampati i dati di tutte le posizioni)
-d da DATA --> '2005-10-01'
-a a DATA --> '2005-10-31'
-m MESE --> none
--anno --> none
-c --> FALSE (vengono stampati solo i dati per la comunicazione
all'ufficio paghe, non quelli di controllo)
-o FILE --> 'paghe.pdf' (il file di output)
--db DB --> 'host=localhost dbname=gampg' (stringa di connessione
al database)
-n --> TRUE (il report viene generato dai dati correnti, non da
quelli storici)
Come si è già detto vi sono essenzialmente due tipi di report (controllo e dettaglio) che vengono utilizzati per la stampa del foglio presenze o per il controllo.
Nota
La procedura mensile delle paghe prevede che contemporaneamente all'invio all'ufficio paghe dei dati definitivi (controllati) del mese, gli stessi vengano consolidati nella tabella DiarioDipendenti del database in modo da poterli recuperare successivamente e stampare con l'opzione -n --no-live.
Stampe
notare nel report precedente i valori negativi della riga flessibilità evidenziati in rosso come è stato impostato in def tabella (vedi Crea la tabella dei dati giornalieri).
Analisi del file sorgente
In questa sezione analizziamo singole parti del sorgente aggiungendo dei commenti a quelli già eventualmente contenuti.
- Importazione delle librerie
- Importazione dei dati dal database
- Manipolazione e predisposizione dei dati
- Definizione delle righe
- Crea la tabella dei dati giornalieri
- Popola la tabella, calcola i dati per i weekend ed i totali delle righe
- Imposta lo stile della tabella
- La sottotabella con il riepilogo dei dati mensili
- Il contenuto delle righe
- Definiamo le altre caratteristiche della pagina
- Opzioni e istruzioni locali
- Ultime istruzioni per la pagina e stampa
Importazione delle librerie
Qui si importano gli strumenti base con cui viene costruito il report.
NB: altre importazioni vengono effettuate in main (vedi sezione Opzioni e istruzioni locali) e riguardano la connessione e l'ambiente di sistema.
La libreria Platypus (Page Layout and Typography) fornisce gli strumenti di alto livello per la definizione della struttura di documenti complessi. Permette quindi la definizione e l'uso di un modello sufficentemente astratto dei singoli elementi che compongono le pagine e di applicare degli stili per gestire il loro layout.
Dalla libreria ReportLab importiamo gli elementi di base per gestire i documenti cartacei e la stampa mentre abbiamo bisogno degli strumenti per gestire le date che prendiamo in datetime. Da decimal importiamo gli strumenti per trattare i numeri.
import os, sys
from datetime import datetime, date, timedelta
from decimal import Decimal
import locale
# importiamo quel po' di Platypus che useremo
from reportlab.platypus import (SimpleDocTemplate, Paragraph,
Spacer, Table, TableStyle,
KeepTogether)
# Importiamo alcune variabili che utilizziamo nell'ambiente
from reportlab.lib.units import cm
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet
Importazione dei dati dal database
Le query di importazione sono SQL dinamiche che vengono utilizzate e ricevono i parametri da python. Sono ovviamente scolpite sul database di riferimento [4].
In particolare la prima SQL usa una funzione del database (diario_dipendente) per ottenere i dati giornalieri mentre la seconda recupera i dati su base mensile (residui ferie, permessi ecc. e eventuali comunicazioni all'Ufficio Paghe).
| [4] | (1, 2) NB: Per l'uso come esempio è possibile attingere i dati da un file csv... |
class ReportPagheDipendente:
"""
Contenitore per i dati di un singolo dipendente.
"""
SQL_DATI_PAGHE = """
SELECT dd.iddipendente, dd.data,
ta.sigla, ta.oraria,
dd.ordinarie, dd.straordinarie,
dd.valoreassenza, dd.dacompensare, dd.flessibilitamaturate,
dd.valoreindennita, dd.oreretribuite+dd.orenonretribuite
FROM diario_dipendente(%(iddip)s, %(dal)s, %(al)s, %(live)s) dd
LEFT JOIN tipologieassenza ta on ta.idtipologiaassenza = dd.idtipologiaassenza
ORDER BY iddipendente, data
"""
SQL_DATI_DIPENDENTE = """
SELECT d.cognome||' '||d.nome, rdl.numeromatricola,
tc.posizioneinps, tc.posizioneinail,
r1.comunicazionireportpresenze,
r.OreFerieResidue + r.OreFerieResidueAP,
r.OreExFestResidue + r.OreExFestResidueAP,
r.OreROLResidue + r.OreROLResidueAP,
r.OreFlessibilitaResidue + r.OreFlessibilitaResidueAP,
r.IndennitaExtra,
r.OreFlessDaLiquidare
FROM Dipendenti d
JOIN RapportiDiLavoro rdl ON rdl.iddipendente = d.iddipendente
JOIN TipologieContratto tc ON tc.IDTipologiaContratto = rdl.IDTipologiaContratto
JOIN Retribuzioni r ON r.IDDipendente = d.IDDipendente
AND r.Mese = %(mese)s
AND r.Anno = %(anno)s
LEFT JOIN Retribuzioni r1 ON r1.IDDipendente = d.IDDipendente
AND r1.Mese = %(mesenota)s
AND r1.Anno = %(annonota)s
WHERE d.IDDipendente = %(iddip)s
"""
Manipolazione e predisposizione dei dati
I contenuti ed il formato dei dati sono condizionati dal database sottostante. Per generare questo report utilizziamo un dataset proveniente da un database postgres [4]. I dati sono quelli contenuti nei record risultanti dalle query descritte nella sezione precedente: Importazione dei dati dal database.
I dati del dataset SQL richiedono alcune trasformazioni. Questo è necessario sia per poterli gestire come entità da inserire negli oggetti python del nostro report sia per eseguire i raggruppamenti e/o inserimenti in dizionari e liste che vengono utilizzati per la stampa.
@classmethod
def caricaDipendenti(cls, conn, iddip, dal, al, live=True):
"""
Carica i dati necessari dal database. Ritorna una sequenza di
istanze di questa classe ordinate per nome.
"""
dipendenti = {}
datidip = conn.cursor()
datipaghe = conn.cursor()
#print cls.SQL % locals()
mese = dal.month-1
anno = dal.year
mesenota = dal.month
annonota = dal.year
if mese<1:
mese = 12
anno -= 1
datipaghe.execute(cls.SQL_DATI_PAGHE, locals())
for row in datipaghe:
(iddip, giorno, siglaassenza, assenzaoraria,
ordinarie, straordinarie, oreassenza,
dacompensare, flessibilita, indennita, lavorate) = row
if iddip in dipendenti:
# Il dipendente c'è già, prendilo
dipendente = dipendenti[iddip]
else:
# Non c'è, crealo e inseriscilo nel dizionario
datidip.execute(cls.SQL_DATI_DIPENDENTE, locals())
(nome_dip, matricola, pinps, pinail,
note, ferieresidue, exfestresidue,
rolresidue, flessresidue, indextra,
flessdaliquidare) = datidip.fetchone()
dipendente = dipendenti[iddip] = cls(nome_dip,
matricola, pinps,
pinail, note,
ferieresidue,
exfestresidue,
rolresidue,
flessresidue,
indextra,
flessdaliquidare)
dipendente.setValoriGiorno(giorno,
dict(ordinarie=ordinarie or 0,
straordinarie=straordinarie or 0,
tipoassenza=siglaassenza,
oreassenza=oreassenza or 0,
dacompensare=dacompensare or 0,
flessibilita=flessibilita or 0,
indennita=indennita or 0,
lavorate=lavorate or 0,
giornaliera=assenzaoraria==False))
conn.close()
return sorted(dipendenti.values(),
cmp=lambda a,b: (cmp(a.posizioneinps,
b.posizioneinps) or
cmp(a.matricola,
b.matricola)))
Questa funzione ritorna la sequenza ordinata delle (serie di) coppie chiave-valore relative ai dati contrattuali, mensili ed ai dati giornalieri dei dipendenti che passeremo agli oggetti del report.
Nota
il decoratore @classmethod trasforma il metodo caricaDipendenti() in un metodo di classe che semplifica l'uso di questa funzione per ottenere la lista dei valori dei dipendenti.
Definizione delle righe
Il metodo __init__ ci consente di definire gli elementi del report ed iniziare ad associare loro i dati. Si definiscono qui le righe di cui è composto il report.
Il metodo __str__ rende disponibili, per ciascun dipendente, gli oggetti precedentemente definiti mentre setValoriGiorno li popola.
def __init__(self, nome, matricola, pinps, pinail, note,
ferieresidue, exfestresidue, rolresidue,
flessresidue, indextra, flessdaliquidare):
self.nome = nome
self.matricola = matricola
self.posizioneinps = pinps
self.posizioneinail = pinail
self.note = note
self.ferieresidue = ferieresidue
self.exfestresidue = exfestresidue
self.rolresidue = rolresidue
self.flessresidue = flessresidue
self.indextra = indextra
self.flessdaliquidare = flessdaliquidare
self.righe = (RigaDati('ordinarie', u'Ordinarie', u'Ordinarie', False),
RigaDati('straordinarie', u'Straordinarie',
u'Straordinarie', False),
RigaTipoAssenze('tipoassenza', u'Tipo assenza',
u'GG Assenza', False),
RigaOreAssenze('oreassenza', u'Ore assenza',
u'Assenze OR', False),
RigaDati('dacompensare',
u'Da compensare', u'Ore Indennità', True),
RigaDati('flessibilita', u'Flessibilità',
u'Flessibilità', True),
RigaDati('indennita', u'Indennità', u'Indennità €', True),
RigaDati('lavorate', u'Ore presenza', u'Ore presenza', True))
def __str__(self):
s = ['Dipendente ' + self.nome]
s.extend([str(riga) for riga in self.righe])
return '\n'.join(s)
def setValoriGiorno(self, giorno, valori):
for riga in self.righe:
riga.setValoreGiorno(giorno, valori)
notare l'uso del prefisso u per trasformare le stringhe in Unicode.
Crea la tabella dei dati giornalieri
Qui definiamo la tabella, ovvero l'elemento principale del report, quello che contiene i dati delle presenze del periodo.
NB: Questo elemento viene generalmente ripetuto più volte nel report, una volta per ciascun Dipendente.
Si può notare l'impostazione della larghezza delle colonne (con colWidths) dei valori giornalieri ([3.8*cm]+[0.5*cm]*len(self.giorni)), dei totali, delle relative etichette e della colonna di riepilogo [1.7*cm,0.8*cm,5*cm])).
Notare anche la colorazione in rosso della riga della flessibilità quando i valori sono negativi mediante l'aggiunta di una proprietà allo stile della cella interessata.
def tabella(self, controllo=False):
"""
Crea la tabella presenze/assenze.
"""
data = self._getDatiTabella(controllo)
stile = self._getStileTabella(controllo)
# per facilitare il controllo cambiamo colore
# ai valori negativi della flessibilità
if controllo:
fless = data[6]
for i,valore in enumerate(fless):
if i>0 and valore < 0:
stile.add('TEXTCOLOR', (i,6), (i, 6), colors.red)
# infine creiamo la tabella e la inseriamo negli elementi
# del documento
t = Table(data, style=stile,
colWidths=[3.8*cm]+[0.5*cm]*len(self.giorni)+[1.7*cm,0.8*cm,5*cm])
return t
Popola la tabella, calcola i dati per i weekend ed i totali delle righe
Nei dati freschi mancano (quasi sempre) i dati relativi ai sabati e alle domeniche (in quanto i Rapportini di Giornata dei Dipendenti riguardano quasi esclusivamente i giorni dal lunedì al venerdì) e quindi, se non ci sono, li dobbiamo genenerare. Se poi esiste il valore delle annotazione mensile, lo dobbiamo aggiungere in fondo alla tabella.
Qui vengono impostate anche le righe da stampare: se il report viene lanciato con l'opzione -c o --controllo la stampa (controllo) comprende quattro righe oltre a quelle del report utilizzato per comunicare le presenze all'ufficio paghe (opzione di default dettaglio). Le righe aggiunte servono per le verifiche dei responsabili di settore e per i controlli di congruità.
Se c'è il campo note la tabella viene automaticamente adattata.
def _getDatiTabella(self, controllo):
"""
Ritorna la sequenza di dati che compongono la tabella.
"""
table_data = []
table_row = [rl_encode(self.nome)]
pivot = self.righe[0]
self.giorni = pivot.giorni()
self.weekends = []
# Intestazione, coi giorni
for i,g in enumerate(self.giorni):
table_row.append('%02d' % g.day)
if g.weekday() >= 5:
self.weekends.append(i)
# Aggiungici il totale
table_row.append('Totali')
table_row.append('')
# E la colonna del riepilogo
table_row.append('Riepilogo')
table_data.append(table_row)
riepilogo = self.riepilogo(controllo)
# Poi ciascuna riga
for riga in self.righe:
if not controllo and riga.controllo:
continue
table_row = [rl_encode(riga.descrizione)]
table_row.extend(riga.dati())
table_row.append(riepilogo)
# le righe successive vengono occupate dalla prima
riepilogo = ''
table_data.append(table_row)
# Se ci sono le note, aggiungile nell'ultima riga della tabella
if self.note:
styles = getSampleStyleSheet()
columns = len(table_row)
table_row = ['',Paragraph(rl_encode(self.note), styles['Normal'])]
table_row.extend([''] * (columns-2))
table_data.append(table_row)
return table_data
Imposta lo stile della tabella
Con questa mappatura si determina la resa finale delle singole celle e parti della tabella.
Ciascuna assegnazione dello stile comprende i riferimenti alla prima e all'ultima cella a cui dare una determinata proprietà. Quindi, per esempio, la riga 'GRID', (1,0),(-2,lastrow),0.25, colors.black) significa "disegna i bordi di tutte le celle comprese tra la seconda (1) della prima riga (0) e la penultima (-2) dell'ultima riga con uno spessore di 0,25 punti e con il colore nero".
Da notare l'uso della proprietà SPAN per le celle delle etichette dei totali, per le celle della colonna riepilogo e, nell'ultimo if, per le celle dove finisce il testo della annotazione mensile. Nella prossima sezione verrà inserita la sottotabella di riepilogo nelle
Notare anche che per colorare lo sfondo delle celle dei sabati e delle domeniche viene usato il riferimento dinamico ai valori calcolati in precedenza da _getDatiTabella().
def _getStileTabella(self, controllo):
"""
Calcola e ritorna lo stile da applicare alla tabella.
"""
if self.note:
lastrow = -2
else:
lastrow = -1
style = TableStyle([('BACKGROUND',(0,0),(-1,0), colors.lightgrey),
('TEXTCOLOR',(0,0),(0,lastrow), colors.red),
('SIZE',(0,1),(-2,lastrow), 7),
# Titolo
('ALIGN',(1,0),(-1,0),'CENTER'),
# Prima colonna
('ALIGN',(0,1),(0,-1),'RIGHT'),
# Ordinarie e straordinarie
('ALIGN',(1,1),(-2,2),'DECIMAL'),
# Tipo assenze
('ALIGN',(1,3),(-3,3),'CENTER'),
('ALIGN',(-2,3),(-2,3),'DECIMAL'),
# Ore assenze e rimanenti
('ALIGN',(1,4),(-4,lastrow),'DECIMAL'),
('GRID', (1,0),(-2,lastrow),0.25, colors.black),
('BOX', (0,0), (-2,lastrow), 0.5, colors.black),
# Colonna totali
('SPAN', (-3,0), (-2,0)),
('ALIGN',(-3,1),(-3,lastrow),'RIGHT'),
('ALIGN',(-2,1),(-2,lastrow),'DECIMAL'),
# Colonna riepilogo
('SPAN', (-1,1), (-1,lastrow)),
('VALIGN', (-1,1), (-1,lastrow), 'MIDDLE'),
])
for c in self.weekends:
style.add('BACKGROUND',
(c+1,1), (c+1,lastrow), colors.lemonchiffon)
if self.note:
style.add('SPAN', (1,-1), (-2,-1))
return style
La sottotabella con il riepilogo dei dati mensili
È la sotto-tabella che viene inserita nell'ultima colonna di destra della tabella principale con i dati di base mensile.
Sono quindi definite anche qui le righe che devono essere stampate nelle due modalità del report (foglio paghe/controllo) e lo stile che devono assumere.
def riepilogo(self, controllo):
indkm = (self.righe[6].totale() +
self.righe[4].totale() * Decimal("5.62"))
data = []
data.append(('Matricola', self.matricola))
data.append(('Posizione INPS', self.posizioneinps))
data.append(('Posizione INAIL', self.posizioneinail))
data.append((rl_encode(u'Indennità km'),
rl_encode(u"€ ")+locale.format("%.2f", indkm)))
data.append((rl_encode(u'Indennità extra'),
rl_encode(u"€ ")+locale.format("%.2f", self.indextra)))
data.append(('Fless. da liquidare', self.flessdaliquidare))
if controllo:
data.append(('', ''))
data.append(('Dati del mese precedente', ''))
data.append(('Ferie residue', self.ferieresidue))
data.append(('Permessi residui', self.exfestresidue))
data.append(('Fless. residua', self.flessresidue))
stile = TableStyle([('ALIGN',(-1,0),(-1,2),'RIGHT'),
('ALIGN',(-1,3),(-1,-1),'DECIMAL'),
('SIZE',(0,0),(-1,-1), 9)
])
if controllo:
stile.add('SPAN',(0,7),(-1,7))
stile.add('ALIGN',(0,7),(0,7),'CENTER')
t = Table(data, rowHeights=[0.35*cm]*len(data), style=stile)
return t
Il contenuto delle righe
Per contenere i dati giornalieri del periodo (ciascuna riga dati della tabella principale) definiamo la classe RigaDati:
class RigaDati:
"""
Contenitore per i dati di una singola riga di un dipendente.
"""
def __init__(self, etichetta, descrizione, desc_totale, controllo):
self.etichetta = etichetta
self.descrizione = descrizione
self.descrizione_totale = desc_totale
self.controllo = controllo
self.giorno = {}
def __str__(self):
s = []
s.append(self.descrizione)
giorni = self.giorno.keys()
giorni.sort()
for giorno in giorni:
s.append('\t%d: %s' % (giorno.day, self.giorno[giorno]))
return ''.join(s)
def setValoreGiorno(self, giorno, valori):
self.giorno[giorno] = valori[self.etichetta]
def giorni(self):
"Ritorna una lista dei giorni ordinati"
giorni = self.giorno.keys()
giorni.sort()
return giorni
def _aggiungiTotale(self, dati):
dati.append(rl_encode(self.descrizione_totale))
dati.append(self.totale())
def dati(self):
"Ritorna una lista dei dati ordinati per giorno, più il totale"
giorni = self.giorni()
dati = [self.giorno[g] or 0 for g in giorni]
self._aggiungiTotale(dati)
return dati
def totale(self):
"Ritorna il totale dato dalla somma di tutti i giorni"
totale = 0
for g in self.giorno:
totale += self.giorno[g] or 0
return totale
che usiamo in Definizione delle righe per sei delle otto righe dati della tabella principale. Per le altre due righe specializziamo la classe precedente nella classe RigaAssenze e poi ulteriormente nelle classi RigaTipoAssenze e RigaOreAssenze che ci daranno i dati con i relativi totali voluti:
class RigaAssenze(RigaDati):
"""
Specializzazione di RigaDati per tenere l'informazione sul tipo di assenza, *giornaliera*
oppure *oraria*.
"""
def __init__(self, etichetta, descrizione, desc_totale, controllo):
RigaDati.__init__(self, etichetta, descrizione, desc_totale, controllo)
self.giornaliera = {}
def setValoreGiorno(self, giorno, valori):
"""
Reimplementa il metodo estendendolo con la gestione della flag "assenza giornaliera"
"""
RigaDati.setValoreGiorno(self, giorno, valori)
self.giornaliera[giorno] = valori['giornaliera']
class RigaTipoAssenze(RigaAssenze):
"""
Specializzazione di RigaAssenze per la riga *tipo assenze*.
"""
def dati(self):
"Ritorna una lista dei dati ordinati per giorno, più il totale"
giorni = self.giorni()
dati = [self.giorno[g] or '' for g in giorni]
self._aggiungiTotale(dati)
return dati
def totale(self):
"Ritorna il numero di assenze giornaliere"
numero = 0
for g in self.giornaliera:
# Se non si tratta di sabati o domeniche...
if self.giornaliera[g] and g.weekday() < 5:
numero += 1
return numero
class RigaOreAssenze(RigaAssenze):
"""
Specializzazione di RigaAssenze per la riga *tipo assenze*.
"""
def totale(self):
"Ritorna il totale delle assenze orarie (non giornaliere)"
totale = 0
for g in self.giornaliera:
# Se non si tratta di sabati o domeniche...
if not self.giornaliera[g] and g.weekday() < 5:
totale += self.giorno[g]
return totale
Definiamo le altre caratteristiche della pagina
Qui impostiamo le informazioni generali sulle pagine del report definendo una prima_pagina con il titolo 'Report paghe' e un sottotitolo calcolato in base ai parametri del report dal e al. Definiamo anche il pie_pagina che dalla seconda pagina in poi inserisce il numero di pagina e la data di esecuzione del report.
def prima_pagina(canvas, doc):
w,h = doc.pagesize
canvas.saveState()
canvas.setFont('Times-Bold', 16)
canvas.drawCentredString(w/2.0, h-30, 'Report paghe')
canvas.setFont('Times-Roman', 9)
canvas.drawCentredString(w/2.0, h-45,
'dal %s al %s' %
(doc.options.dal.strftime('%d %B %Y'),
doc.options.al.strftime('%d %B %Y')))
canvas.restoreState()
pie_pagina(canvas, doc, first=True)
def pie_pagina(canvas, doc, first=False):
canvas.saveState()
canvas.setFont('Times-Roman', 6)
canvas.drawString(1*cm, 0.5*cm, 'Pagina %d' % doc.page)
w,h = doc.pagesize
if not first:
canvas.drawCentredString(w/2.0, 0.5*cm,
'Report paghe dal %s al %s' %
(doc.options.dal.strftime('%d/%m/%Y'),
doc.options.al.strftime('%d/%m/%Y')))
canvas.drawString(w-100, 0.5*cm, datetime.now().strftime('%c'))
Opzioni e istruzioni locali
In main() importiamo le funzioni di sistema per poter gestire le opzioni a riga di comando, per trasformare l'opzione -m nelle opzioni dal, al e per effettuare quindi la connessione al database.
def main():
import sys
from optparse import OptionParser
from psycopg2 import connect
locale.setlocale(locale.LC_ALL, '')
parser = OptionParser()
parser.add_option('-D', '--dipendente', default=None,
help="ID del dipendente, per stamparne solo uno")
parser.add_option('-I', '--inps', default=None,
help="Posizione INPS, per stampare solo quelli")
parser.add_option('-d', '--dal', default="2005-10-01",
help="Data inizio periodo", metavar="DATA")
parser.add_option('-a', '--al', default="2005-10-31",
help="Data fine periodo", metavar="DATA")
parser.add_option('-m', '--mese', metavar="MESE",
help="Specifica il periodo con il mese di competenza")
parser.add_option('--anno', metavar="ANNO",
help="Specifica l'anno, se diverso da quello corrente")
parser.add_option('-c', '--controllo', default=False,
help="Specifica se stampare le righe di controllo",
action="store_true")
parser.add_option('-o', '--output', default='paghe.pdf',
help="Nome del file PDF di output", metavar="FILE")
parser.add_option('--db', default='host=localhost dbname=gampg',
help="Specifica la string di connessione al GAMPG",
metavar="DB")
parser.add_option('-n', '--no-live', default=True, action="store_false",
dest="live",
help="Prendi i dati storici anziché calcolarli "
"dai dati correnti")
options, args = parser.parse_args()
if options.mese:
if options.anno:
anno = int(options.anno)
else:
oggi = date.today()
anno = oggi.year
options.dal = date(anno, int(options.mese), 1)
options.al = ((options.dal + timedelta(31)).replace(day=1) -
timedelta(1))
else:
options.dal = date(*map(int, options.dal.split('-')))
options.al = date(*map(int, options.al.split('-')))
conn = connect(options.db)
Ultime istruzioni per la pagina e stampa
Con queste ultime istruzioni definiamo le dimensioni della carta su cui stampiamo, i bordi, i margini, gli spazi per il titolo e poi invochiamo la classe principale ReportPagheDipendente che esegue effettivamente il report popolando le tabelle che sono prima costruite e poi tenute assieme dalla riga elementi.append(KeepTogether(tabella).
Notare come sia in questa sezione che viene applicata l'azione di filtro sulla posizione inps dei dipendenti (-I INPS) limitando la costruzione alle tabelle dei Dipendenti di una determinata posizione I.N.P.S..
# imposta le caratteristiche generali della pagina la disposizione
# in orizzontale, i margini
doc = SimpleDocTemplate(options.output,
pagesize=landscape(A4), showBoundary=1,
leftMargin=10, rightMargin=10, topMargin=10,
bottomMargin=10)
doc.options = options
# Lascia uno spazio iniziale per il titolo
elementi = [Spacer(0, 2*cm)]
dipendenti = ReportPagheDipendente.caricaDipendenti(conn,
options.dipendente,
options.dal,
options.al,
options.live)
for dipendente in dipendenti:
if options.inps and options.inps <> dipendente.posizioneinps:
continue
tabella = dipendente.tabella(controllo=options.controllo)
elementi.append(KeepTogether(tabella))
elementi.append(Spacer(0, 0.5*cm))
# Facciamo il render degli elementi e salviamo
doc.build(elementi, onFirstPage=prima_pagina, onLaterPages=pie_pagina)
if __name__ == '__main__':
main()