Calcoli & formule
Algoritmi business estratti dal codice Spring, replicabili byte-per-byte
1. Calcolo germinabilità media (cuore del software)
Metodo: AnalisiServiceImpl.generaConta(AnalisiBO bo), righe 377-482 del file AnalisiServiceImpl.java.
Input
- Una lista di
ContaBO(tipicamente 4, una per ripetizione A/B/C/D) - Ogni conta ha 2 letture:
giorni_1(prima) egiorni_2(finale) - Per ogni lettura:
germinati,duri,freschi,anormali,morti(int) - Vigore:
vigore_1,vigore_2∈ {SCARSO, MEDIO, OTTIMO}
Algoritmo — pseudo-codice esatto
mapVigore = {SCARSO=0, MEDIO=1, OTTIMO=2, null=0}
mapVigoreI = {0=SCARSO, 1=MEDIO, 2=OTTIMO}
germinato1 = 0; germinato2 = 0
duro1 = 0; duro2 = 0
fresco1 = 0; fresco2 = 0
anormale1 = 0; anormale2 = 0
morto1 = 0; morto2 = 0
vigore1 = 0; vigore2 = 0
# --- accumulo per tutte le conte ---
for conta in conte: # tipicamente 4 ripetizioni
germinato1 += conta.germinati_1
germinato2 += conta.germinati_2
duro1 += conta.duri_1
duro2 += conta.duri_2
fresco1 += conta.freschi_1
fresco2 += conta.freschi_2
anormale1 += conta.anormali_1
anormale2 += conta.anormali_2
morto1 += conta.morti_1
morto2 += conta.morti_2
vigore1 += mapVigore[conta.vigore_1]
# per vigore2: se germinato2 > 0 usa vigore_2, altrimenti usa vigore_1
if germinato2 > 0:
vigore2 += mapVigore[conta.vigore_2]
else:
vigore2 += mapVigore[conta.vigore_1]
# --- medie (arrotondamento verso zero) ---
size = len(conte)
germinato1I = int(germinato1 / size)
germinato2I = int(germinato2 / size)
duro1I = int(duro1 / size); duro2I = int(duro2 / size)
fresco1I = int(fresco1 / size); fresco2I = int(fresco2 / size)
anormale1I = int(anormale1 / size); anormale2I = int(anormale2 / size)
morto1I = int(morto1 / size); morto2I = int(morto2 / size)
# --- normalizzazione: somma totale deve essere 100 ---
sommaI = germinato1I + duro1I + fresco1I + anormale1I + morto1I
sommaII = germinato2I + duro2I + fresco2I + anormale2I + morto2I
if sommaII > 0:
sommaTotale = sommaI + sommaII
if sommaTotale > 100:
germinato2I -= (sommaTotale - 100) # sottrai l'eccesso
elif sommaTotale < 100:
germinato2I += (100 - sommaTotale) # aggiungi la differenza
# --- germinabilità finale (valore stored in DB) ---
germinabilita = germinato1I + germinato2I
# --- vigore finale ---
vigoreI = int((vigore1 + vigore2) / (size * 2))
vigore = mapVigoreI[vigoreI]
- Arrotondamento Java
(int)= truncate (verso zero), NON round. Es.(int)(99.9/4) = 24, non 25. - La normalizzazione "ruba" al germinato2: il secondo valore di germinati è il "campo sacrificabile" per far quadrare il 100%. Se la somma fa 101, viene tolto 1 da germinati_2. Se fa 99, ne viene aggiunto 1.
Esempio numerico
4 ripetizioni con questi valori:
| Ripetiz. | g1 | d1 | f1 | a1 | m1 | g2 | d2 | f2 | a2 | m2 |
|---|---|---|---|---|---|---|---|---|---|---|
| A | 80 | 2 | 3 | 0 | 0 | 10 | 1 | 2 | 1 | 1 |
| B | 82 | 1 | 2 | 1 | 0 | 9 | 2 | 1 | 1 | 1 |
| C | 78 | 3 | 3 | 1 | 0 | 11 | 1 | 2 | 0 | 1 |
| D | 81 | 2 | 2 | 0 | 0 | 10 | 2 | 1 | 1 | 1 |
| Σ | 321 | 8 | 10 | 2 | 0 | 40 | 6 | 6 | 3 | 4 |
| media (int) | 80 | 2 | 2 | 0 | 0 | 10 | 1 | 1 | 0 | 1 |
SommaI = 80+2+2+0+0 = 84. SommaII = 10+1+1+0+1 = 13. Totale = 97 < 100.
Normalizzazione: germinati_2 += (100 - 97) = 3 → germinati_2 = 13.
Germinabilità finale = 80 + 13 = 93%
2. Generazione FT (fattura tecnica)
Metodo: AnalisiServiceImpl.nuova(LottoBO lotto), righe 94-110.
Formato
NNN/YY dove NNN è progressivo globale nell'anno, YY sono le ultime 2 cifre dell'anno.
Algoritmo
anno = formato(now(), "yy") // es. "26" per 2026
# Query Hibernate Criteria (DAO):
ultimoFT = SELECT ft
FROM semiorto_analisi
WHERE ft LIKE '%/26'
ORDER BY id DESC
LIMIT 1
if ultimoFT == null or ultimoFT == "" or "/" not in ultimoFT:
numero = 1
else:
numero = int(ultimoFT.split("/")[0]) + 1
ft = f"{numero}/{anno}" // es. "3562/26"
Il codice non usa lock DB. Se due tecnici creano contemporaneamente nuova analisi con FT, è possibile collisione. In pratica non è mai successo perché Semiorto ha 4 tecnici e il carico è basso.
In TopSeed suggeriamo di usare SELECT ... FOR UPDATE o sequence DB dedicata.
3. Conta pre-popolata alla creazione analisi
Quando il tecnico apre "Nuova Analisi", il sistema crea una ContaBO A con valori pre-compilati usando i parametri della specie del lotto:
c = new ContaBO()
c.init() # azzera tutti i campi a 0
c.getGiorni().clear()
c.getGiorni().add( specie.giorni1Conta ) # es. 5
c.getGiorni().add( specie.giorniTConta - specie.giorni1Conta ) # es. 10 - 5 = 5
c.setGiorniT( specie.giorniTConta ) # es. 10
Quindi per una specie con giorni1Conta=5, giorniTConta=10:
- Ripetizione A preimpostata con giorni_1=5, giorni_2=5 (delta rispetto alla prima conta)
- giorniT = 10 (giorno della conta finale)
4. Prossima operazione (badge lista "In corso")
Metodo: AnalisiServiceImpl.inCorso(), righe 122-151.
Per ogni analisi "in corso", calcola quando è la prossima operazione:
c = Calendar.getInstance()
c.setTime(analisi.dataInizio)
if analisi.semiortoContes.isEmpty():
# nessuna conta ancora → prossima è la 1ª conta
c.add(DAY, specie.giorni1conta)
prossimaOperazione = "(1°)"
dataProssimaOperazione = c.getTime()
else:
# almeno una conta fatta → prossima è la 2ª (o finale)
c.add(DAY, specie.giorniTconta)
prossimaOperazione = "(2°)"
dataProssimaOperazione = c.getTime()
5. Generazione codice lotto (dead code?)
Metodo LottoServiceImpl.generaLotto(LottoBO bo), righe 170-190. Esiste ma non viene invocato da nessun controller della nostra copia sorgenti.
Algoritmo
s = ""
# Lettera anno: 'A'=2025, 'B'=2026, 'C'=2027, ...
# Formula: (anno in 2 cifre + 64) come carattere ASCII
data = int(format(dataArrivo, "yy")) + 64
s += chr(data)
# Esempio: 2020 → 20+64=84 → 'T'
# Esempio: 2025 → 25+64=89 → 'Y' ??? (si aspetta 'A')
# NOTA: la formula potrebbe essere sbagliata, o presupporre un anno base
# diverso (es. anno - 1985 + 64 = offset per A=2025?)
# Codice specie (uppercase)
s += toUpper(specie.codice)
# Codice varietà (uppercase)
s += toUpper(varieta.codice)
# Codice produttore (uppercase)
s += toUpper(produttore.codice)
return s
Tre problemi con questa funzione:
- Non viene chiamata dal controller. La JSP di creazione lotto chiede al tecnico di inserire il codice manualmente.
- La formula lettera-anno è sospetta.
20+64=84è 'T', non 'A'. Bisognerebbe chiedere a Diego se intendevaanno - 1960o simile. - È plausibile che sia codice incompleto abbandonato: il dev lo ha scritto per un'altra feature (codice auto) poi non completata.
Nella migrazione TopSeed: ignorare, mantenere input manuale del codice lotto come nel comportamento attuale.
6. Check "lotto già esistente"
Metodo LottoServiceImpl.lottoDisponibile(LottoBO bo).
def lottoDisponibile(bo):
return !lottoDao.isLottoExists(bo.lotto, bo.id)
# DAO:
def isLottoExists(codiceLotto, idEscluso):
SELECT COUNT(*)
FROM semiorto_lotto
WHERE id = :codiceLotto
AND (id_lotto != :idEscluso OR :idEscluso IS NULL)
Usato nel controller LottoController.update(): se il codice esiste E l'utente non ha spuntato forza=true, mostra warning.
7. Password hashing
Classe: Spring Security ShaPasswordEncoder (SHA-1).
encoder = new ShaPasswordEncoder()
# Hashing nuova password
hash = encoder.encodePassword(rawPassword, null) # salt = null!
# Verifica (automatico da Spring Security in login)
matches = encoder.isPasswordValid(storedHash, rawPassword, null)
SHA-1 senza salt è deprecato. Attacchi rainbow table comuni.
Nella migrazione TopSeed:
- Importa hash come
legacy_sha_hash - Al primo login dopo migrazione, se password digitata matcha SHA-1 legacy → rehash a bcrypt e salva in
password_hash - Dopo X mesi, force reset per chi non ha ancora fatto login
8. Placeholder certificato DOCX
Metodo AnalisiController.certificato(). I 20 placeholder sostituiti nel template:
| Placeholder nel DOCX | Valore Java | Formato |
|---|---|---|
$$_NOME_SPECIE_$$ | lotto.specie.nome("en") | inglese |
$$_NOME_LATINO_$$ | lotto.specie.nome("lat") | latino |
$$_NOME_VARIETA_$$ | lotto.varieta.nome | — |
$$_NOME_LOTTO_$$ | lotto.lotto | codice testuale |
$$_NOME_CATEGORIA_$$ | analisi.categoria | STANDARD / COMMERCIALE |
$$_DATA_ARRIVO_$$ | dataArrivoLaboratorio | dd/MM/yyyy |
$$_DATA_RICEZIONE_$$ | dataInizio | dd/MM/yyyy |
$$_DATA_FINE_$$ | dataFine (chiusura) | dd/MM/yyyy |
$$_FT_$$ | ft | es. "3562/26" |
$$_SEME_PURO_$$ | semePuro | % |
$$_MATERIALE_INERTE_$$ | materialeInerte | % |
$$_ALTRI_SEMI_$$ | "0.0" | Hardcoded! Bug noto: non usa il valore DB |
$$_NUMERO_GIORNI_$$ | conta.giorniT | int |
$$_SEMI_GERMINATI_$$ | conta.germinatiT (=germ1+germ2) | int (0-100) |
$$_SEMI_DURI_$$ | conta.duriT | int |
$$_SEMI_FRESCHI_$$ | conta.freschiT | int |
$$_SEMI_ANORMALI_$$ | conta.anormaliT | int |
$$_SEMI_MORTI_$$ | conta.mortiT | int |
$$_UMIDITA_$$ | analisi.ur | % |
$$_NOME_TECNICO_$$ | tecnico.nome | — |
$$_DATA_$$ | now() | "April 20, 2026" |
9. Anni disponibili (per dropdown cerca)
SELECT YEAR(data) as anno
FROM semiorto_analisi
GROUP BY anno
ORDER BY anno IS NULL DESC, anno DESC
Questo restituisce tutti gli anni in cui esistono analisi, + NULL in cima (rappresentato come "In corso").
10. Calcolo valori totali conte (colonne *T)
Nel BO ContaBO, i campi *T (totali) sono calcolati così:
anormaliT = anormali_1 + anormali_2
duriT = duri_1 + duri_2
freschiT = freschi_1 + freschi_2
germinatiT = germinati_1 + germinati_2
mortiT = morti_1 + morti_2
if germinati_2 != null or anormali_2 != null or duri_2 != null
or freschi_2 != null or morti_2 != null:
giorniT = giorni_2
else:
giorniT = giorni_1 # se non c'è seconda lettura, giorniT = giorno della prima
11. Vigore della conta singola (media di vigore_1 e vigore_2)
vigore = 0
if vigore_1: vigore += mapVigore[vigore_1] # 0/1/2
if vigore_2: vigore += mapVigore[vigore_2]
vigore = int(vigore / 2) # media
result = mapVigoreI[vigore] # converti indice → stringa
Esempio: vigore_1=OTTIMO (2), vigore_2=MEDIO (1) → (2+1)/2 = 1 → MEDIO.