Personal tools
You are here: Home Lavori e servizi Laboratorio informatico ReportLab
Document Actions

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.

comunicazione_presenze

Un esempio di comunicazione mensile relativa ad un Dipendente

[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.

report_controllo_dettaglio

Dettaglio del report di controllo relativo ad un Dipendente

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.

report_fp_dettaglio

Dettaglio del report foglio paghe relativo ad un Dipendente

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

report_fp

Una pagina del report foglio paghe

report_controllo

Una pagina del report di controllo delle presenze

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

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()

Powered by Plone CMS, the Open Source Content Management System

This site conforms to the following standards: