feat: Implement price list management and import functionality

- Added ListinoRelationManager for managing price lists associated with models.
- Introduced ImportaListiniAction for importing price lists from Excel files.
- Created ListinoPrezzo model to represent price entries in the database.
- Developed ListinoExcelParser for parsing Excel files containing price lists.
- Implemented ListinoImporter to handle the import logic and associations.
- Added migration for creating listino_prezzi table and converting measures to mm.
- Updated frontend logic to handle new price list features and validations.
- Enhanced tests for price list import and conversion functionalities.
This commit is contained in:
2026-10-19 16:51:24 +02:00
parent a8aa6d9927
commit 1e352eb2ef
46 changed files with 1575 additions and 327 deletions
+5 -6
View File
@@ -112,19 +112,18 @@ const Configurator = {
let val_maniglia = $(`input[name=maniglia]:checked`).val();
let finitura_maniglia = shared_data.data_maniglie.find(m => m.id == val_maniglia);
// Prima fascia della tabella con altezza (mm) >= a quella del serramento.
let altezza = parseFloat(config.configurazione.height);
let mod_altezza = altezza % 10;
if(mod_altezza > 0) altezza = (altezza - mod_altezza) + 10;
let altezza_maniglia = shared_data.altezza_maniglia.find((m) => m.altezza == altezza);
let altezza_maniglia = [...shared_data.altezza_maniglia]
.sort((a, b) => a.altezza - b.altezza)
.find((m) => m.altezza >= altezza);
if(altezza_maniglia != null){
config.configurazione.altezza_maniglia = altezza_maniglia.altezza_maniglia;
}
if(altezza_maniglia == null && shared_data.altezza_maniglia.length > 0){
config.configurazione.altezza_maniglia = shared_data.altezza_maniglia[0];
config.configurazione.altezza_maniglia = shared_data.altezza_maniglia[0].altezza_maniglia;
}
if(finitura_maniglia != null)
+6 -4
View File
@@ -103,8 +103,10 @@ const MenuBuilder = {
const minLarghezza = Math.min(...listino.map(item => item.larghezza));
const minAltezza = Math.min(...listino.map(item => item.altezza));
$('.titolo').html(`${params.nome}`);
$('.larghezza-max').html(`${minLarghezza}-${maxLarghezza}cm`);
$('.altezza-max').html(`${minAltezza}-${maxAltezza}cm`);
$('.larghezza-max').html(`${minLarghezza}-${maxLarghezza} mm`);
$('.altezza-max').html(`${minAltezza}-${maxAltezza} mm`);
$('#larghezza').attr({ min: minLarghezza, max: maxLarghezza });
$('#altezza').attr({ min: minAltezza, max: maxAltezza });
window.sistema = data.sistema;
@@ -210,8 +212,8 @@ const MenuBuilder = {
*/
let size = PriceCalculator.getMinSize();
$('#larghezza').val(size.larghezza + '.00');
$('#altezza').val(size.altezza + '.00');
$('#larghezza').val(size.larghezza);
$('#altezza').val(size.altezza);
+16 -13
View File
@@ -25,24 +25,27 @@ const PriceCalculator = {
return firstSize;
},
// Misure in mm: entro min/max del listino si usa la prima misura >= a quella
// richiesta (le griglie non sono sempre a passo regolare, es. traslanti da 1450 mm).
getGridPrice: (width, height) => {
let altezza = parseFloat(height);
let larghezza = parseFloat(width);
const larghezza = PriceCalculator.misuraListino('larghezza', width);
const altezza = PriceCalculator.misuraListino('altezza', height);
if (larghezza == null || altezza == null) return -1;
const misure = listini.misure.find(el => el.altezza == altezza && el.larghezza == larghezza);
let mod_altezza = altezza % 10;
let mod_larghezza = larghezza % 10;
if(mod_altezza > 0) altezza = (altezza - mod_altezza) + 10;
if(mod_larghezza > 0) larghezza = (larghezza - mod_larghezza) + 10;
let misure = listini.misure.find((el) => {
if(el.altezza == altezza && el.larghezza == larghezza)
return el;
});
if(misure == null) return -1;
return misure.prezzo;
},
misuraListino: (campo, valore) => {
const v = parseFloat(valore);
if (isNaN(v)) return null;
const misure = [...new Set(listini.misure.map(el => el[campo]))].sort((a, b) => a - b);
// Min/max del listino vincolanti: fuori range la misura non è disponibile.
if (!misure.length || v < misure[0]) return null;
return misure.find(m => m >= v) ?? null;
},
getConfigurationPrice: (data) => {
let prezzo_finale = data.prezzo_base;
+5 -1
View File
@@ -173,7 +173,11 @@ const start = async () => {
const data_maniglie = modello.moduli.includes('maniglia') ? (await (await fetch(`${modello_folder}/${modello.cartella}/maniglia.json?rnd=${Math.random()}`)).json()).filter(el => el.id != "") : [];
const data_avvolgibile = modello.moduli.includes('avvolgibile') ? (await (await fetch(`${modello_folder}/${modello.cartella}/avvolgibile.json?rnd=${Math.random()}`)).json()).filter(el => el.id != "") : [];
const data_vetri = modello.moduli.includes('vetro') ? (await (await fetch(`${modello_folder}/${modello.cartella}/vetro.json?rnd=${Math.random()}`)).json()).filter(el => el.id != "") : [];
const listino = modello.moduli.includes('listino') ? await (await fetch(`${modello_folder}/${modello.cartella}/listino.json?rnd=${Math.random()}`)).json() : [];
const listino = modello.moduli.includes('listino')
? await (await fetch(`${PREVENTIVI_API_BASE}/sistemi/${encodeURIComponent(params.sistema)}/modelli/${encodeURIComponent(modello.id)}/listino`, { headers: { Accept: 'application/json' } })).json()
: [];
if (modello.moduli.includes('listino') && !listino.length)
throw new Error(`Nessun listino caricato per il modello ${modello.id}`);
const altezza_maniglia = modello.moduli.includes('altezza_maniglia') ? await (await fetch(`${data_folder}/altezza_maniglia_${modello.categoria}.json?rnd=${Math.random()}`)).json() : [];
let priceCalculator = await MenuBuilder.build(data, 'left-container', modello.cartella_texture || modello.cartella, {