4d936d2ee3
App Flutter de gestion de stock/prix pour tablette Android (100% local) : scan code-barres (OpenFoodFacts), prix au kilo/litre, packs, inventaire, prix d'achat + marge, filtres de tri, étiquettes PDF, design noir & blanc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
107 lines
3.2 KiB
Dart
107 lines
3.2 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:pdf/pdf.dart';
|
|
import 'package:pdf/widgets.dart' as pw;
|
|
|
|
import '../models/produit.dart';
|
|
|
|
/// Génère une planche A4 d'étiquettes de prix (nom, prix TTC, prix HT, code-barres),
|
|
/// imprimable sur une imprimante classique (ex : Canon TS4550).
|
|
class EtiquettePdf {
|
|
/// Construit le document PDF pour les [produits] sélectionnés.
|
|
static Future<Uint8List> construire(List<Produit> produits) async {
|
|
final doc = pw.Document();
|
|
|
|
doc.addPage(
|
|
pw.MultiPage(
|
|
pageFormat: PdfPageFormat.a4,
|
|
margin: const pw.EdgeInsets.all(12),
|
|
build: (context) {
|
|
return [
|
|
pw.Wrap(
|
|
spacing: 6,
|
|
runSpacing: 6,
|
|
children: produits.map(_etiquette).toList(),
|
|
),
|
|
];
|
|
},
|
|
),
|
|
);
|
|
|
|
return doc.save();
|
|
}
|
|
|
|
static pw.Widget _etiquette(Produit p) {
|
|
// 3 colonnes sur A4 (~186mm utiles / 3 ≈ 62mm).
|
|
const largeur = 185.0; // points (~65mm)
|
|
const hauteur = 110.0; // points (~39mm)
|
|
|
|
return pw.Container(
|
|
width: largeur,
|
|
height: hauteur,
|
|
padding: const pw.EdgeInsets.all(8),
|
|
decoration: pw.BoxDecoration(
|
|
border: pw.Border.all(color: PdfColors.grey400, width: 0.7),
|
|
borderRadius: pw.BorderRadius.circular(6),
|
|
),
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
pw.Text(
|
|
p.titre,
|
|
maxLines: 2,
|
|
overflow: pw.TextOverflow.clip,
|
|
style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold),
|
|
),
|
|
pw.Container(
|
|
width: double.infinity,
|
|
alignment: pw.Alignment.center,
|
|
child: pw.Column(
|
|
mainAxisSize: pw.MainAxisSize.min,
|
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
|
children: [
|
|
pw.Text(
|
|
_prix(p.prix),
|
|
style: pw.TextStyle(
|
|
fontSize: 22, fontWeight: pw.FontWeight.bold),
|
|
),
|
|
if (p.prixMesure != null)
|
|
pw.Text(
|
|
_prixMesure(p),
|
|
style: const pw.TextStyle(
|
|
fontSize: 9, color: PdfColors.grey700),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
_codeBarres(p.codeBarres),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
static pw.Widget _codeBarres(String? code) {
|
|
if (code == null || code.isEmpty) {
|
|
return pw.SizedBox(height: 22);
|
|
}
|
|
final estEan13 = code.length == 13 && int.tryParse(code) != null;
|
|
return pw.BarcodeWidget(
|
|
barcode: estEan13 ? pw.Barcode.ean13() : pw.Barcode.code128(),
|
|
data: code,
|
|
width: 150,
|
|
height: 26,
|
|
drawText: true,
|
|
textStyle: const pw.TextStyle(fontSize: 6),
|
|
);
|
|
}
|
|
|
|
static String _prix(double v) => '${v.toStringAsFixed(2).replaceAll('.', ',')} EUR';
|
|
|
|
static String _prixMesure(Produit p) {
|
|
final m = p.prixMesure!;
|
|
final unite = m.suffixe == '/kg' ? 'kg' : 'L';
|
|
return 'soit ${m.valeur.toStringAsFixed(2).replaceAll('.', ',')} EUR / $unite';
|
|
}
|
|
}
|