Version initiale de Gestion Prix
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>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
/// Unités de vente supportées (poids et volume).
|
||||
class Unites {
|
||||
static const g = 'g';
|
||||
static const kg = 'kg';
|
||||
static const ml = 'ml';
|
||||
static const cl = 'cl';
|
||||
static const l = 'L';
|
||||
|
||||
/// Ancienne valeur (produits d'avant) : vendu à l'unité. Plus proposée.
|
||||
static const piece = 'piece';
|
||||
|
||||
static const tout = [g, kg, ml, cl, l];
|
||||
|
||||
/// Vrai si le code est une unité de mesure valide (donc pas 'piece'/null).
|
||||
static bool estValide(String? code) => tout.contains(code);
|
||||
|
||||
static String libelle(String? code) => switch (code) {
|
||||
g => 'g',
|
||||
kg => 'kg',
|
||||
ml => 'ml',
|
||||
cl => 'cl',
|
||||
l => 'L',
|
||||
_ => '',
|
||||
};
|
||||
}
|
||||
|
||||
/// Un produit du stock.
|
||||
class Produit {
|
||||
final String id;
|
||||
final String? codeBarres;
|
||||
final String nom;
|
||||
final String? imageUrl;
|
||||
|
||||
/// Prix de vente (au client).
|
||||
final double prix;
|
||||
|
||||
/// Prix d'achat (coût). 0 = non renseigné.
|
||||
final double prixAchat;
|
||||
|
||||
/// Contenance d'UN contenant (ex : 330 pour 330 ml, 400 pour 400 g).
|
||||
final double? quantite;
|
||||
|
||||
/// Unité de la quantité : voir [Unites].
|
||||
final String? unite;
|
||||
|
||||
/// Nombre de contenants pour un pack (ex : 6 pour un pack de 6 bières).
|
||||
/// Null ou 1 = produit à l'unité (pas un pack).
|
||||
final int? nbContenants;
|
||||
|
||||
/// Horodatage de création (ms) — sert au tri par ordre d'ajout.
|
||||
final int creeLe;
|
||||
|
||||
/// Quantité en stock (inventaire).
|
||||
final int stock;
|
||||
|
||||
const Produit({
|
||||
required this.id,
|
||||
this.codeBarres,
|
||||
required this.nom,
|
||||
this.imageUrl,
|
||||
required this.prix,
|
||||
this.prixAchat = 0,
|
||||
this.quantite,
|
||||
this.unite,
|
||||
this.nbContenants,
|
||||
this.creeLe = 0,
|
||||
this.stock = 0,
|
||||
});
|
||||
|
||||
bool get estPack => (nbContenants ?? 1) > 1;
|
||||
|
||||
/// Gain en euros (vente − achat). Pertinent seulement si un prix d'achat est saisi.
|
||||
double get gain => prix - prixAchat;
|
||||
|
||||
/// Coefficient multiplicateur (prix de vente ÷ prix d'achat), ex : 1,5.
|
||||
/// Null si le prix d'achat n'est pas renseigné. Sert au tri.
|
||||
double? get coefficient => prixAchat > 0 ? prix / prixAchat : null;
|
||||
|
||||
/// Marge affichée : le coefficient (1,5) suivi d'un « % », ex : « 1,5 % », « 2 % ».
|
||||
/// Vide si le prix d'achat n'est pas renseigné.
|
||||
String get margeLibelle {
|
||||
final c = coefficient;
|
||||
if (c == null) return '';
|
||||
var s = c.toStringAsFixed(2).replaceAll('.', ',');
|
||||
if (s.contains(',')) {
|
||||
s = s.replaceAll(RegExp(r'0+$'), '').replaceAll(RegExp(r',$'), '');
|
||||
}
|
||||
return '$s %';
|
||||
}
|
||||
|
||||
Produit copyWith({
|
||||
String? id,
|
||||
String? codeBarres,
|
||||
String? nom,
|
||||
String? imageUrl,
|
||||
double? prix,
|
||||
double? prixAchat,
|
||||
double? quantite,
|
||||
String? unite,
|
||||
int? nbContenants,
|
||||
int? creeLe,
|
||||
int? stock,
|
||||
}) {
|
||||
return Produit(
|
||||
id: id ?? this.id,
|
||||
codeBarres: codeBarres ?? this.codeBarres,
|
||||
nom: nom ?? this.nom,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
prix: prix ?? this.prix,
|
||||
prixAchat: prixAchat ?? this.prixAchat,
|
||||
quantite: quantite ?? this.quantite,
|
||||
unite: unite ?? this.unite,
|
||||
nbContenants: nbContenants ?? this.nbContenants,
|
||||
creeLe: creeLe ?? this.creeLe,
|
||||
stock: stock ?? this.stock,
|
||||
);
|
||||
}
|
||||
|
||||
/// Prix à l'unité de mesure (€/kg ou €/L) — obligation légale pour les
|
||||
/// produits vendus au poids/volume. Null pour les produits à la pièce.
|
||||
({double valeur, String suffixe})? get prixMesure {
|
||||
final q = quantite;
|
||||
if (q == null || q <= 0) return null;
|
||||
// Pour un pack, on calcule sur le volume/poids TOTAL (contenance × nombre).
|
||||
final total = q * (nbContenants ?? 1);
|
||||
return switch (unite) {
|
||||
Unites.g => (valeur: prix / (total / 1000), suffixe: '/kg'),
|
||||
Unites.kg => (valeur: prix / total, suffixe: '/kg'),
|
||||
Unites.ml => (valeur: prix / (total / 1000), suffixe: '/L'),
|
||||
Unites.cl => (valeur: prix / (total / 100), suffixe: '/L'),
|
||||
Unites.l => (valeur: prix / total, suffixe: '/L'),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Libellé compact de la quantité, ex : « 330ml », « 1kg », « 75cl »,
|
||||
/// ou « 6x33cl » pour un pack. Vide si absent.
|
||||
String get quantiteLibelle {
|
||||
final q = quantite;
|
||||
if (q == null || q <= 0 || !Unites.estValide(unite)) return '';
|
||||
final n = q == q.roundToDouble()
|
||||
? q.toInt().toString()
|
||||
: q.toString().replaceAll('.', ',');
|
||||
final base = '$n${Unites.libelle(unite)}';
|
||||
return estPack ? '${nbContenants}X$base' : base;
|
||||
}
|
||||
|
||||
/// Titre affiché (liste + étiquette) : la quantité est intégrée devant le nom,
|
||||
/// ex : « 1kg Farine », « 75cl Coca-Cola ». Sinon juste le nom.
|
||||
String get titre {
|
||||
final ql = quantiteLibelle;
|
||||
return ql.isEmpty ? nom : '$ql $nom';
|
||||
}
|
||||
|
||||
factory Produit.fromMap(Map<String, dynamic> map) {
|
||||
return Produit(
|
||||
id: map['id'].toString(),
|
||||
codeBarres: map['code_barres'] as String?,
|
||||
nom: (map['nom'] as String?) ?? '',
|
||||
imageUrl: map['image_url'] as String?,
|
||||
prix: (map['prix'] as num?)?.toDouble() ?? 0,
|
||||
prixAchat: (map['prix_achat'] as num?)?.toDouble() ?? 0,
|
||||
quantite: (map['quantite'] as num?)?.toDouble(),
|
||||
unite: map['unite'] as String?,
|
||||
nbContenants: (map['nb_contenants'] as num?)?.toInt(),
|
||||
creeLe: (map['cree_le'] as num?)?.toInt() ?? 0,
|
||||
stock: (map['stock'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
/// Map pour insertion/mise à jour dans la base (sans l'id, généré par SQLite).
|
||||
Map<String, dynamic> toInsertMap() {
|
||||
return {
|
||||
'code_barres': codeBarres,
|
||||
'nom': nom,
|
||||
'image_url': imageUrl,
|
||||
'prix': prix,
|
||||
'prix_achat': prixAchat,
|
||||
'quantite': quantite,
|
||||
'unite': unite,
|
||||
'nb_contenants': nbContenants,
|
||||
'stock': stock,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user