Seconda puntata di Faccio cose, vedo gente, la serie sui plugin WordPress fatti su misura per i clienti, e primo dei due episodi dedicati allo stesso negozio WooCommerce: un’utensileria che vende a officine, ferramenta e aziende, con prezzi diversi per ogni tipo di cliente. Il problema di partenza era classico del B2B: servivano listini prezzi WooCommerce per cliente, più di venti, importati dal gestionale, mostrati solo a chi è loggato e applicati davvero nel carrello, al posto di uno snippet che faceva uno sconto percentuale e basta.
In questo articolo ti racconto come ho trasformato un CSV «largo» con una colonna per listino in una tabella con lo storico per data, come ho convinto il plugin a leggere prezzi scritti come «€ 68,25», perché valido tutti gli SKU prima di scrivere una riga, come il listino arriva al prodotto, alle varianti e al carrello, e quali bug mi hanno insegnato qualcosa su WooCommerce. Il codice è pubblico su GitHub, senza i dati del cliente, con il nome Customers Price List for WooCommerce.
Venti listini con prezzi per codice prodotto
Il vecchio sito applicava uno sconto percentuale per utente con un filtro sul prezzo e una voce nel carrello. Il gestionale invece ragiona per listini prezzi: ogni cliente ha un codice e ogni articolo ha un prezzo per codice.
Quando ho preso in mano il sito, i prezzi riservati funzionavano con uno snippet nel tema: a ogni utente era associata una percentuale di sconto, un filtro modificava il prezzo mostrato e una voce negativa nel carrello sistemava il totale. Semplice, finché il commerciale non ha spiegato come lavora davvero l’azienda: il gestionale ha più di venti listini, con codici come L40, L45, L50N1, L55N1_NF, e un articolo può costare il 40 per cento in meno in un listino e il 52 in un altro, con arrotondamenti e prezzi netti decisi a tavolino. Una percentuale sola non lo rappresenta.
Il secondo problema era l’aggiornamento. I prezzi cambiano, i listini prezzi si aggiornano con un export dal gestionale, e nessuno vuole ricopiarli in WooCommerce. Il terzo era la visibilità: chi non è cliente non deve vedere i prezzi, perché i prezzi di listino sono un’informazione commerciale, e non deve poter comprare. Il quarto, meno detto ma importante, era lo storico: se un cliente contesta un prezzo bisogna sapere che listino valeva quel giorno.
Da qui il plugin, il primo dei due per questo cliente. L’altro, che racconterò in una delle prossime puntate, gestisce le giacenze di più magazzini dallo stesso gestionale: due problemi diversi, stesso schema di lavoro, un CSV che esce dal gestionale e un plugin che lo legge, lo controlla e lo porta in WooCommerce senza mani sulla tastiera.

Da un CSV largo a una tabella lunga
Il gestionale esporta i listini prezzi in un CSV con una colonna per listino. Il plugin lo trasforma in righe (SKU, codice listino, prezzo, data) in una tabella dedicata, così i listini nuovi non richiedono cambi allo schema e lo storico cresce da solo.
Il file che arriva dal gestionale ha questa forma: una colonna SKU, una colonna LORDO con il prezzo pieno e poi una colonna per ogni listino, con il codice del listino come intestazione. È il formato che chiamo largo, comodo da leggere per un umano e scomodissimo da interrogare: per sapere il prezzo di un articolo in un listino dovrei conoscere il nome della colonna, e ogni listino nuovo cambierebbe la struttura.
SKU;LORDO;L40;L45;L50N1;L55
UT-1001;€ 289,00;€ 173,40;€ 158,95;€ 144,50;€ 130,05
UT-1002;€ 59,90;€ 35,94;€ 32,95;€ 29,95;€ 26,96Il plugin fa la trasformazione da largo a lungo: ogni cella diventa una riga in una tabella dedicata, custom_price_lists, con lo SKU, il codice del listino, il prezzo come DECIMAL(15,4), una data di validità e uno stato. La colonna LORDO non finisce nella tabella: aggiorna il prezzo pieno di WooCommerce, quello che vedrebbe un cliente senza listino. Un indice unico su SKU, codice e data fa sì che l’import sia un UPSERT: reimportare lo stesso file non duplica nulla, aggiorna.
La data è il dettaglio che fa lo storico. Ogni import scrive le righe con la data di oggi; in lettura si prende la riga più recente per SKU e codice. Così un import al mese produce un archivio dei listini mese per mese, interrogabile con una query, e la domanda «quanto costava a marzo?» ha una risposta. È anche la scelta che rende indolori i listini nuovi, perché lo schema non cambia mai: se domani il gestionale aggiunge una colonna L60, il plugin la vede, crea il codice e via; non c’è una tabella da modificare né un’opzione da aggiungere.
Validazione dei prezzi
Il primo import vero ha scartato migliaia di prezzi perché erano scritti con il simbolo dell’euro e la virgola. Da allora il plugin accetta i formati che escono dai gestionali, non quelli dei manuali.
La prima versione del plugin leggeva i prezzi con un floatval, come insegnano gli esempi. Il primo import vero, a febbraio, con migliaia di righe e una ventina di listini, ha riempito il log di avvisi: «invalid price format» su quasi tutte le colonne. Il gestionale esportava i prezzi come «€ 68,25», con il simbolo, lo spazio e la virgola decimale, e per PHP quella stringa vale zero. L’import tecnicamente era riuscito e commercialmente era un disastro, perché i prodotti avevano il prezzo pieno e nessun listino.
Da lì la funzione che normalizza i prezzi è diventata una delle più testate del progetto. Toglie i simboli di valuta, gli spazi e i caratteri strani; poi ragiona sui separatori con una regola semplice, l’ultimo separatore vince: in «1.234,56» l’ultimo è la virgola, quindi è il decimale e il punto è delle migliaia; in «1,234.56» vale il contrario; in «68,25» la virgola è decimale. Il risultato è un numero con il punto, pronto per il database.
Lo stesso import ha insegnato altre due cose. La prima: gli avvisi vanno raggruppati per riga, perché un messaggio per ogni colonna sbagliata, moltiplicato per venti listini e migliaia di righe, produce un log che nessuno legge; ora una riga con prezzi illeggibili genera un avviso solo, con l’elenco delle colonne. La seconda: il separatore del CSV va rilevato, non imposto, perché Excel italiano esporta con il punto e virgola e il gestionale con la virgola, e il BOM UTF-8 in testa al file va tolto prima di leggere la prima intestazione, altrimenti la colonna «SKU» si chiama «SKU» e non la trova nessuno.

Verifica degli SKU
Tutti gli SKU del file dei listini prezzi vengono cercati su WooCommerce prima di scrivere qualunque cosa: gli articoli che non esistono vengono saltati e segnalati una volta sola, e un import non lascia mai il catalogo a metà.
Un file di listini arriva dal gestionale, e il gestionale conosce articoli che il sito non ha: fuori catalogo, ricambi, codici interni. Se l’import scrivesse riga per riga scoprendo gli SKU mancanti strada facendo, un file con un errore a metà lascerebbe il catalogo in uno stato misto, con i primi articoli aggiornati e gli altri no. Per questo il plugin fa due passaggi: prima raccoglie tutti gli SKU unici del file e li cerca in WooCommerce, poi importa solo le righe degli articoli che esistono. Gli SKU mancanti finiscono nel report una volta ciascuno, non una volta per colonna.
Anche la pulizia degli SKU ha avuto il suo bug. La funzione che li normalizzava toglieva tutto ciò che non fosse lettera, numero, punto o trattino, e nel catalogo esistono codici come A95.2073340+6, con il più che indica una variante. Il più spariva, lo SKU diventava un altro e il report diceva «non trovato» per un articolo che c’era. Aggiungere un carattere alla lista dei permessi è stato facile; capire perché proprio quegli articoli restassero senza listino, un po’ meno.
Il risultato è che un import è un’operazione tranquilla: si carica il file, si guarda il report, si capisce cosa manca, si sistema il catalogo o il file e si rilancia, e l’UPSERT fa sì che rilanciare non costi nulla. Per le prove sui file grandi c’è un campo che fissa il numero massimo di righe da leggere, così si verifica la struttura in pochi secondi prima di lanciare l’import completo.
Listini prezzi per ogni cliente
Ogni utente ha nel profilo il codice del suo listino. In frontend il prezzo mostrato è quello del listino, chi non ha un listino vede il prezzo pieno, chi non è loggato non vede prezzi e non può comprare.
Il collegamento tra persone e listini è volutamente semplice: nel profilo utente c’è un menu a tendina «Listino assegnato» con i codici presenti nella tabella, e la scelta finisce in un meta dell’utente. Niente ruoli, niente gruppi: il commerciale sa che l’officina Rossi è un L40 e lo imposta, e i listini prezzi seguono la persona in tutto il negozio. Se un giorno servisse un’assegnazione automatica, per esempio dal CRM, basta scrivere quel meta via REST o con un hook.
In frontend lavora una classe che risolve il prezzo: dato uno SKU e un utente, legge il codice del listino, cerca la riga più recente nella tabella e restituisce il prezzo, con un filtro wcpl_final_price che permette di intervenire prima della visualizzazione. I risultati, anche quelli negativi, restano in una cache per la durata della richiesta, così una pagina di categoria con quaranta prodotti non fa quaranta volte la stessa domanda al database per un utente senza listino.
I visitatori anonimi non vedono prezzi: al posto del prezzo compare un invito ad accedere, personalizzabile con un filtro, e il prodotto non è acquistabile grazie al filtro woocommerce_is_purchasable. È una scelta commerciale del cliente, non tecnica, e il plugin la rispetta senza nascondere il catalogo: le schede si vedono, si cercano, si confrontano, solo il prezzo aspetta il login. Per chi è loggato ma senza listino vale il prezzo pieno di WooCommerce, che è anche il prezzo che appare quando un articolo non è nel listino dell’utente.

Varianti, carrello e prezzo zero
I prodotti variabili usano un filtro diverso per il prezzo del padre, il carrello va ricalcolato a ogni caricamento e un prezzo zero in tabella deve valere come «nessun prezzo».
Il carrello è il punto in cui un plugin di prezzi mostra se è serio. Non basta cambiare il prezzo visualizzato: il totale si calcola dai prezzi degli oggetti nel carrello, che WooCommerce rilegge dal prodotto. Per questo il plugin si aggancia a woocommerce_before_calculate_totals e, a ogni calcolo, reimposta il prezzo di ogni riga con quello del listino dell’utente. Se cambi listino a un utente mentre ha il carrello pieno, al prossimo caricamento i totali sono già giusti.
Il primo bug istruttivo riguarda i prodotti variabili. Il prezzo del prodotto padre, quello che vedi prima di scegliere la variante, passa dal filtro woocommerce_variable_price_html e non da woocommerce_get_price_html, che il plugin intercettava. Risultato: le varianti mostravano il listino, il padre no. E quando una variante aveva un prezzo scontato impostato a zero per errore, il padre mostrava 0,00 euro. Ora il plugin raccoglie i prezzi di listino delle varianti visibili e mostra un prezzo solo o un intervallo da minimo a massimo, e il listino vince sempre sullo sconto impostato sul prodotto.
Il secondo bug è un classico dei dati importati: una riga di listino con prezzo zero, artefatto di un export. La funzione che risolve il prezzo restituiva 0.0, un numero valido per PHP, e il frontend mostrava articoli gratis. Da allora qualunque valore uguale o inferiore a zero, in qualunque forma arrivi dal database, vale come «nessun prezzo personalizzato» e fa scattare il prezzo pieno.
Il terzo è quasi comico: il CSS del plugin non si caricava perché dipendeva dal foglio di stile woocommerce-general, che il tema del cliente deregistra; WordPress, se una dipendenza non esiste, salta in silenzio lo stile che la richiede. Tolta la dipendenza, tutto a posto.

Import da terminale con dry run
Il comando wp price-list import legge il file dei listini prezzi o un URL, con la modalità dry run che valida tutto senza scrivere e una tabella riassuntiva; messo in un cron, l’aggiornamento dei listini non richiede più nessuno.
L’interfaccia nell’admin va bene per chi carica un file ogni tanto, ma il caso tipico di questo cliente è l’export notturno dal gestionale. Per quello c’è WP-CLI: wp price-list import –file=listino.csv oppure –url= per un file pubblicato su un indirizzo, con la stessa pipeline dell’admin (stesso parser, stessa validazione, stesso report) e un riepilogo in tabella alla fine. L’opzione –format produce tabella, CSV, JSON o YAML, così l’output può finire in un log o in un sistema di monitoraggio.
wp price-list import --file=/percorso/listino.csv --dry-run
wp price-list import --url=https://gestionale.esempio.it/export/listino.csv
wp price-list statusLa modalità dry run è quella che uso sempre per prima: legge il file, lo trasforma, cerca gli SKU, conta tutto e non scrive nulla. Se il report torna con zero errori e gli SKU mancanti sono quelli attesi, si rilancia senza l’opzione. Il comando status dice quante righe attive ci sono in tabella e quali codici di listino esistono, utile per capire al volo se l’ultimo import è passato. Il tutto, messo in un cron di sistema, trasforma l’aggiornamento dei listini prezzi WooCommerce in una cosa che succede di notte e di cui si legge il log al mattino, lo stesso approccio che uso per le automazioni con n8n.

Log, sicurezza e piccole cose
Log giornalieri protetti, un visore in stile terminale con il download, una scheda per sfogliare la tabella dei listini, nonce e capability ovunque e un controllo contro il path traversal sui file di log.
Ogni import scrive un log giornaliero in una cartella dentro uploads, protetta da un file .htaccess, e la scheda Log mostra gli ultimi trenta import con un visore a sfondo scuro e un pulsante per scaricare il file. La scheda Dati sfoglia la tabella dei listini a pagine di cinquanta righe, con il filtro per SKU e la tendina dei codici: è il modo più rapido per rispondere al commerciale che chiede «ma per l’articolo tal dei tali il listino L45 è entrato?».
Sul fronte sicurezza non c’è nulla di eroico, solo le cose che vanno fatte: nonce e capability manage_woocommerce su ogni azione dell’admin, sanificazione degli input, query preparate. Il visore e il download dei log validano il nome del file con un’espressione regolare e con realpath() contro il path traversal, perché un endpoint che legge «il file che gli dici» è un invito. L’import dall’admin segue il pattern post, redirect, get, con il risultato in un transient, così il refresh non reimporta e le preferenze del modulo (origine, URL, numero di righe) vengono ricordate per ciascun amministratore.
Due dettagli che non c’entrano con i prezzi ma che il cliente ha apprezzato: il plugin è tradotto in italiano con un piccolo compilatore di file .mo scritto in PHP, perché sull’hosting non c’era msgfmt, e carica Select2 sui filtri della sidebar del negozio, con la ricerca sempre visibile, perché con centinaia di marche una tendina normale è inutilizzabile. Sono le cose piccole che ti chiedono mentre fai quella grande, e a cui conviene dire di sì.
Cosa manca ancora
Gli hook del frontend non girano nelle chiamate admin-ajax, i prezzi WooCommerce vengono scritti nei meta senza la CRUD e l’import va riga per riga: tre cose da sistemare prima di usarlo su un negozio più grande.
La prima è una scelta di bootstrap che si è rivelata un po’ troppo furba: il plugin registra gli hook del frontend solo quando non è in admin. Peccato che alcune chiamate del negozio, per esempio certi mini carrelli e frammenti aggiornati via admin-ajax.php, passino per un contesto che WordPress considera admin. In quelle risposte il prezzo sarebbe quello pieno. Il tema del cliente non le usa, ma su un altro sito è la prima cosa che controllerei.
La seconda riguarda il prezzo pieno della colonna LORDO: il plugin lo scrive direttamente nei meta del prodotto invece di passare dalla CRUD di WooCommerce. Funziona, ma la tabella di lookup che WooCommerce usa per ordinare e filtrare per prezzo può restare indietro finché qualcosa non la rigenera. La terza è la velocità dell’import: le righe vengono scritte una per una, e con molte migliaia di celle un import dura minuti. Un INSERT multiplo a blocchi di cento righe lo accorcerebbe parecchio, e il commento nel codice che lo ammette c’è già.
Quello che rifarei uguale: la tabella lunga con la data, la validazione degli SKU prima di scrivere, il dry run e il principio che un prezzo strano, zero o negativo, non è mai un prezzo. Sono le quattro cose che rendono i listini prezzi WooCommerce di questo plugin affidabili anche quando il file che arriva non lo è.
Domande frequenti
Come si gestiscono listini prezzi WooCommerce diversi per cliente?
Con una tabella dedicata che associa SKU, codice del listino e prezzo, e un codice di listino assegnato a ogni utente. In frontend il prezzo mostrato e quello del carrello sono quelli del listino dell’utente; chi non ha un listino vede il prezzo pieno.
I listini prezzi funzionano con i prodotti variabili?
Sì: ogni variante ha il suo SKU e quindi il suo prezzo di listino, e il prodotto padre mostra un prezzo o un intervallo calcolato sulle varianti visibili, tramite il filtro woocommerce_variable_price_html.
Cosa succede ai visitatori non loggati?
Non vedono i prezzi e non possono aggiungere al carrello: al posto del prezzo compare un invito ad accedere, personalizzabile. Il catalogo resta visibile e navigabile.
Che formato deve avere il CSV dei listini prezzi?
Una colonna SKU, una colonna LORDO con il prezzo pieno e una colonna per ogni listino, con il codice come intestazione. Il separatore viene rilevato da solo e i prezzi possono essere scritti con il simbolo dell’euro e la virgola decimale.
Si può automatizzare l’aggiornamento dei listini prezzi WooCommerce?
Sì, con il comando WP-CLI wp price-list import, che legge un file o un URL e si può mettere in un cron. La modalità dry run permette di validare il file senza scrivere nulla.
Si conserva lo storico dei prezzi?
Sì: ogni import scrive le righe con la data del giorno e in lettura si usa la più recente. La tabella diventa un archivio dei listini prezzi nel tempo, interrogabile per SKU, codice e data.
Il plugin è compatibile con HPOS?
Sì, dichiara la compatibilità con l’archiviazione ordini ad alte prestazioni di WooCommerce e non tocca le tabelle degli ordini: lavora sui prezzi dei prodotti e sul calcolo del carrello.
Il prezzo giusto alla persona giusta
Una tabella lunga con la data, un parser che accetta i prezzi come arrivano, la validazione prima della scrittura e il carrello sempre ricalcolato: così i listini prezzi WooCommerce reggono anche con venti codici e un export notturno.
Lo snippet con lo sconto percentuale è andato in pensione, il commerciale assegna i listini dal profilo utente e il gestionale manda il file; il resto lo fa il plugin.
Alla fine i listini prezzi WooCommerce non sono un problema di prezzi ma di dati: formati che arrivano come arrivano, codici che non esistono, zeri che non sono prezzi.
La cosa importante è avere un log che ti dica cosa è successo, così da poterlo replicare e correggere.
Per approfondire: il repository su GitHub ha il README con il formato del CSV, lo schema della tabella e gli hook per sviluppatori, e un changelog con la storia dei bug raccontati qui; la documentazione per sviluppatori di WooCommerce spiega i filtri sui prezzi e la CRUD dei prodotti; e nella categoria WordPress trovi la prima puntata della serie, quella del plugin delle prenotazioni.
Se il tuo negozio vende a clienti con prezzi diversi e oggi lo gestisci con un foglio di calcolo e la pazienza, prenota una call e guardiamo insieme come esce il file dal tuo gestionale; nella pagina sulla consulenza WooCommerce trovi gli altri progetti B2B e come lavoro.
E tu, come gestisci i prezzi riservati sul tuo e-commerce: ruoli, coupon, un plugin, un foglio Excel inviato per email? Raccontalo nei commenti, soprattutto se hai un formato di export più strano di «€ 68,25». E se conosci un commerciale che ogni mese ricopia i prezzi a mano in WooCommerce, mandagli questo articolo: te ne sarà grato, dopo averti odiato un attimo.


