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:
@@ -13,8 +13,8 @@ class ConfigurazioneFormatter
|
||||
{
|
||||
/** Etichette italiane per le chiavi note. */
|
||||
private const LABELS = [
|
||||
'width' => 'Larghezza (cm)',
|
||||
'height' => 'Altezza (cm)',
|
||||
'width' => 'Larghezza (mm)',
|
||||
'height' => 'Altezza (mm)',
|
||||
'tipo_finitura' => 'Tipo finitura',
|
||||
'finitura_interna' => 'Finitura interna',
|
||||
'finitura_esterna' => 'Finitura esterna',
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\Listini;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
|
||||
/**
|
||||
* Legge il file Excel dei listini (primo foglio). Il file contiene più tabelle una
|
||||
* sotto l'altra, ciascuna nella forma (misure in mm, come salvate a sistema):
|
||||
*
|
||||
* codice_infisso | 400 | 500 | ... ← larghezze in mm
|
||||
* 400 | p | p | ... ← altezza in mm + prezzi
|
||||
* ...
|
||||
*
|
||||
* Se la riga del titolo non contiene larghezze, l'intestazione è sulla riga successiva
|
||||
* (es. "CASSONETTO PVC" seguito da "mm | 500 | 600 ...").
|
||||
*/
|
||||
class ListinoExcelParser
|
||||
{
|
||||
/**
|
||||
* @return list<array{codice: string, prezzi: list<array{larghezza: int, altezza: int, prezzo: float}>}>
|
||||
*/
|
||||
public static function parse(string $path): array
|
||||
{
|
||||
$reader = IOFactory::createReader(IOFactory::identify($path));
|
||||
$reader->setReadDataOnly(true);
|
||||
|
||||
$righe = $reader->load($path)->getSheet(0)->toArray(null, true, false, false);
|
||||
|
||||
return self::parseRighe($righe);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<list<mixed>> $righe
|
||||
*/
|
||||
public static function parseRighe(array $righe): array
|
||||
{
|
||||
$blocchi = [];
|
||||
$n = count($righe);
|
||||
|
||||
for ($r = 0; $r < $n; $r++) {
|
||||
$titolo = $righe[$r][0] ?? null;
|
||||
if (! is_string($titolo) || trim($titolo) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$intestazione = $r;
|
||||
$larghezze = self::larghezze($righe[$r]);
|
||||
if (! $larghezze && isset($righe[$r + 1])) {
|
||||
$intestazione = $r + 1;
|
||||
$larghezze = self::larghezze($righe[$r + 1]);
|
||||
}
|
||||
if (! $larghezze) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$prezzi = [];
|
||||
for ($rr = $intestazione + 1; $rr < $n; $rr++) {
|
||||
$altezza = $righe[$rr][0] ?? null;
|
||||
if (! is_numeric($altezza)) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($larghezze as $colonna => $larghezza) {
|
||||
$prezzo = $righe[$rr][$colonna] ?? null;
|
||||
if (! is_numeric($prezzo)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$prezzi[] = [
|
||||
'larghezza' => (int) round($larghezza),
|
||||
'altezza' => (int) round((float) $altezza),
|
||||
'prezzo' => round((float) $prezzo, 2),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($prezzi) {
|
||||
$blocchi[] = ['codice' => trim($titolo), 'prezzi' => $prezzi];
|
||||
}
|
||||
|
||||
$r = $rr - 1;
|
||||
}
|
||||
|
||||
return $blocchi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Larghezze consecutive a partire dalla colonna B, indicizzate per colonna.
|
||||
*
|
||||
* @return array<int, float>
|
||||
*/
|
||||
private static function larghezze(array $riga): array
|
||||
{
|
||||
$larghezze = [];
|
||||
for ($c = 1; $c < count($riga) && is_numeric($riga[$c]); $c++) {
|
||||
$larghezze[$c] = (float) $riga[$c];
|
||||
}
|
||||
|
||||
return $larghezze;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support\Listini;
|
||||
|
||||
use App\Models\Modello;
|
||||
use App\Models\Sistema;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ListinoImporter
|
||||
{
|
||||
// Codici del file con refusi noti rispetto al codice interno del modello.
|
||||
private const ALIAS = [
|
||||
'finestra_wasistas' => 'finestra_vasistas',
|
||||
];
|
||||
|
||||
/**
|
||||
* Associazione proposta codice-del-file → modello del sistema (null se nessun
|
||||
* modello corrisponde): per codice interno, poi per nome o descrizione.
|
||||
*
|
||||
* @param list<string> $codici
|
||||
* @return array<string, int|null>
|
||||
*/
|
||||
public static function suggerisciAssociazioni(Sistema $sistema, array $codici): array
|
||||
{
|
||||
$modelli = $sistema->modelli()->get();
|
||||
$associazioni = [];
|
||||
|
||||
foreach ($codici as $codice) {
|
||||
$associazioni[$codice] = self::trovaModello($modelli, $codice)?->id;
|
||||
}
|
||||
|
||||
return $associazioni;
|
||||
}
|
||||
|
||||
private static function trovaModello(Collection $modelli, string $codice): ?Modello
|
||||
{
|
||||
$slug = Str::slug($codice, '_');
|
||||
$slug = self::ALIAS[$slug] ?? $slug;
|
||||
|
||||
return $modelli->first(fn (Modello $m) => $m->codice === $slug)
|
||||
?? $modelli->first(fn (Modello $m) => Str::slug($m->nome, '_') === $slug)
|
||||
?? $modelli->first(fn (Modello $m) => $m->descrizione && Str::slug($m->descrizione, '_') === $slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sostituisce il listino di ogni modello associato con i prezzi del rispettivo blocco.
|
||||
*
|
||||
* @param list<array{codice: string, prezzi: list<array>}> $blocchi
|
||||
* @param array<string, int|null> $associazioni codice del file → id modello
|
||||
* @return int numero di modelli aggiornati
|
||||
*/
|
||||
public static function importa(Sistema $sistema, array $blocchi, array $associazioni): int
|
||||
{
|
||||
$prezziPerCodice = collect($blocchi)->keyBy('codice')->map(fn (array $b) => $b['prezzi']);
|
||||
$modelli = $sistema->modelli()->get()->keyBy('id');
|
||||
|
||||
return DB::transaction(function () use ($associazioni, $prezziPerCodice, $modelli) {
|
||||
$aggiornati = 0;
|
||||
$ora = now();
|
||||
|
||||
foreach ($associazioni as $codice => $modelloId) {
|
||||
$modello = $modelloId ? $modelli->get($modelloId) : null;
|
||||
if (! $modello || ! $prezziPerCodice->has($codice)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$modello->listino()->delete();
|
||||
// Una stessa misura ripetuta nel blocco: vale l'ultima.
|
||||
$righe = collect($prezziPerCodice[$codice])
|
||||
->keyBy(fn (array $p) => $p['larghezza'].'x'.$p['altezza'])
|
||||
->map(fn (array $p) => $p + ['modello_id' => $modello->id, 'created_at' => $ora, 'updated_at' => $ora])
|
||||
->values();
|
||||
foreach ($righe->chunk(500) as $chunk) {
|
||||
$modello->listino()->insert($chunk->all());
|
||||
}
|
||||
|
||||
$modello->update(['listino_aggiornato_at' => $ora]);
|
||||
$aggiornati++;
|
||||
}
|
||||
|
||||
return $aggiornati;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user