From 6fb76ebed61d468c8c88e229800be2cddc94d17b Mon Sep 17 00:00:00 2001 From: Vincenzo Nikodem Cesqui Podiani Date: Sun, 11 Oct 2026 14:29:14 +0200 Subject: [PATCH] Patch a funzionamento registrazione vendite --- include/logic.h | 173 ++---- src/logic.c | 1148 ++++++++++++++++---------------------- src/windowsDeclaration.c | 10 +- 3 files changed, 543 insertions(+), 788 deletions(-) diff --git a/include/logic.h b/include/logic.h index d921984..9f2b9ef 100644 --- a/include/logic.h +++ b/include/logic.h @@ -15,165 +15,124 @@ struct product { char name; }; +/* + * Modello dati: nel file prodotti si salva solo la giacenza INIZIALE ("initial"). + * Lo stock corrente e' sempre calcolato: initial - copie vendute + * (comprese quelle consumate tramite bundle). Le vendite scrivono solo il file vendite. + */ + /* ------------------------------------------------------------------ */ /* Prodotti */ /* ------------------------------------------------------------------ */ /** - * @brief Restituisce l'array JSON contenente tutti i prodotti. + * @brief Restituisce l'array JSON contenente tutti i prodotti, cosi' come salvati su file + * (quindi con "initial" e senza stock corrente). * @param fname Percorso del file JSON dei prodotti. - * @return Puntatore all'oggetto JSON (array) dei prodotti (da liberare con json_object_put()), oppure NULL in caso di errore. + * @return Array JSON (da liberare con json_object_put()), oppure NULL in caso di errore. */ json_object *getProducts(const char *fname); /** - * @brief Stampa a schermo (tramite ncurses) la lista dei prodotti con relativo prezzo, inventario e tag bundle. - * @param fname Percorso del file JSON dei prodotti. + * @brief Come getProducts, ma ogni prodotto ha in piu' il campo "stock" calcolato + * (-1 = infinito; i bundle hanno sempre -1). E' una vista in memoria: non va salvata. + * @param productsFile Percorso del file JSON dei prodotti. + * @param soldFile Percorso del file JSON delle vendite (se manca, si assume nessuna vendita). + * @return Array JSON (da liberare con json_object_put()), oppure NULL in caso di errore. */ -void listProducts(const char *fname); +json_object *getProductsWithStock(const char *productsFile, const char *soldFile); /** * @brief Aggiunge un nuovo prodotto con prezzo e inventario illimitato. - * @param fname Percorso del file JSON dei prodotti. - * @param name Nome del prodotto. - * @param price Prezzo del prodotto. - * @return 0 in caso di successo, 1 se il prodotto esiste già, -1 in caso di errore. + * @return 0 in caso di successo, 1 se il prodotto esiste gia', -1 in caso di errore. */ int addProduct(const char *fname, const char *name, double price); /** - * @brief Aggiunge un nuovo prodotto specificando prezzo e quantità iniziale in magazzino. - * @param fname Percorso del file JSON dei prodotti. - * @param name Nome del prodotto. - * @param price Prezzo del prodotto. - * @param stock Quantità iniziale in magazzino (-1 per inventario illimitato). - * @return 0 in caso di successo, 1 se il prodotto esiste già, -1 in caso di errore. + * @brief Aggiunge un nuovo prodotto specificando prezzo e giacenza iniziale. + * @param stock Giacenza iniziale (negativo per inventario illimitato). + * @return 0 in caso di successo, 1 se il prodotto esiste gia', -1 in caso di errore. */ int addProductWithInventory(const char *fname, const char *name, double price, int stock); /** - * @brief Aggiunge un prodotto composto (bundle) costituito da altri componenti. - * @param fname Percorso del file JSON dei prodotti. - * @param name Nome del bundle. - * @param price Prezzo del bundle. - * @param components Oggetto JSON (array) descrivente i componenti e le relative quantità. - * @return 0 in caso di successo, 1 se il prodotto esiste già, -1 in caso di errore. + * @brief Aggiunge un prodotto composto (bundle). + * @param components Array JSON di {"name": ..., "qty": ...}; il bundle ne prende possesso in caso di successo. + * @return 0 in caso di successo, 1 se il prodotto esiste gia', -1 in caso di errore. */ int addBundleProduct(const char *fname, const char *name, double price, json_object *components); /** - * @brief Ottiene il prezzo di un prodotto specifico. - * @param fname Percorso del file JSON dei prodotti. - * @param name Nome del prodotto da cercare. - * @return Il prezzo del prodotto, oppure -1.0 se non trovato o in caso di errore. + * @brief Ottiene il prezzo di un prodotto. + * @return Il prezzo, oppure -1.0 se non trovato o in caso di errore. */ double getPrice(const char *fname, const char *name); /** - * @brief Aggiorna nome e prezzo di un prodotto esistente, sincronizzando anche le vendite se il nome cambia. - * @param productsFile Percorso del file JSON dei prodotti. - * @param soldFile Percorso del file JSON delle vendite. - * @param oldName Nome attuale del prodotto. - * @param newName Nuovo nome da assegnare al prodotto. - * @param price Nuovo prezzo del prodotto. - * @return 0 in caso di successo, codice di errore/stato altrimenti. + * @brief Aggiorna nome e prezzo di un prodotto lasciando invariato lo stock corrente. + * @return 0 ok, 1 non trovato, 2 nome gia' usato, -1 errore. */ int updateProduct(const char *productsFile, const char *soldFile, const char *oldName, const char *newName, double price); /** - * @brief Aggiorna nome, prezzo e inventario di un prodotto esistente. - * @param productsFile Percorso del file JSON dei prodotti. - * @param soldFile Percorso del file JSON delle vendite. - * @param oldName Nome attuale del prodotto. - * @param newName Nuovo nome da assegnare. - * @param price Nuovo prezzo. - * @param stock Quantità in magazzino. - * @return 0 in caso di successo, codice di errore/stato altrimenti. + * @brief Aggiorna nome, prezzo e stock di un prodotto. + * Se il nome cambia aggiorna nella stessa operazione file vendite e riferimenti nei bundle. + * @param stock Giacenza ATTUALE desiderata (negativo = infinita). + * @return 0 ok, 1 non trovato, 2 nome gia' usato, -1 errore. */ int updateProductWithInventory(const char *productsFile, const char *soldFile, const char *oldName, const char *newName, double price, int stock); /** - * @brief Legge lo stato dell'inventario (quantità e tracciamento) di un prodotto. - * @param fname Percorso del file JSON dei prodotti. + * @brief Legge lo stock corrente (calcolato) di un prodotto. + * @param productsFile Percorso del file JSON dei prodotti. + * @param soldFile Percorso del file JSON delle vendite. * @param name Nome del prodotto. - * @param stock Puntatore a intero in cui salvare la quantità di magazzino. - * @param tracked Puntatore a intero in cui salvare se l'inventario è tracciato (1) o meno (0). - * @return 0 in caso di successo, 1 se il prodotto non viene trovato, -1 in caso di errore. + * @param stock Quantita' corrente (PRODUCT_STOCK_UNLIMITED se non tracciato). + * @param tracked 1 se l'inventario e' tracciato, 0 altrimenti. + * @return 0 ok, 1 prodotto non trovato, -1 errore. */ -int getProductInventory(const char *fname, const char *name, int *stock, int *tracked); - -/** - * @brief Imposta la quantità e lo stato di tracciamento dell'inventario per un prodotto. - * @param fname Percorso del file JSON dei prodotti. - * @param name Nome del prodotto. - * @param stock Quantità di magazzino da impostare. - * @param tracked Flag di tracciamento (1 per tracciato, 0 altrimenti). - * @return 0 in caso di successo, 1 se il prodotto non è trovato, -1 in caso di errore. - */ -int setProductInventory(const char *fname, const char *name, int stock, int tracked); - -/** - * @brief Consuma (scala) una determinata quantità dall'inventario di un prodotto o dei suoi componenti (se bundle). - * @param fname Percorso del file JSON dei prodotti. - * @param name Nome del prodotto. - * @param qty Quantità da consumare. - * @return 0 in caso di successo, codice di errore o disponibilità insufficiente altrimenti. - */ -int consumeProductInventory(const char *fname, const char *name, int qty); +int getProductInventory(const char *productsFile, const char *soldFile, const char *name, int *stock, int *tracked); /* ------------------------------------------------------------------ */ /* Copie vendute */ /* ------------------------------------------------------------------ */ /** - * @brief Restituisce il numero di copie vendute di un determinato prodotto. - * @param fname Percorso del file JSON delle vendite. - * @param name Nome del prodotto. - * @return Il numero di copie vendute (0 se non presente, -1 in caso di errore). + * @brief Numero di copie vendute di un prodotto. + * @return Copie vendute (0 se non presente, -1 in caso di errore). */ int getCount(const char *fname, const char *name); /** - * @brief Stampa a schermo (tramite ncurses) la lista delle vendite registrate per ciascun prodotto. - * @param fname Percorso del file JSON delle vendite. + * @brief Stampa a schermo (ncurses) le vendite registrate per ciascun prodotto. */ void listSold(const char *fname); /** - * @brief Aggiunge una quantità di copie vendute per un prodotto (creando la voce se non esiste). - * @param fname Percorso del file JSON delle vendite. - * @param name Nome del prodotto. - * @param qty Quantità venduta da aggiungere. - * @return 0 in caso di successo, -1 in caso di errore. + * @brief Aggiunge copie vendute per un prodotto (crea la voce se non esiste). + * Non controlla lo stock: per questo usare recordSale(). + * @return 0 ok, -1 errore. */ int addSold(const char *fname, const char *name, int qty); /** - * @brief Elimina la registrazione delle vendite di un prodotto e ripristina l'inventario corrispondente. - * @param productsFile Percorso del file JSON dei prodotti. - * @param soldFile Percorso del file JSON delle vendite. - * @param name Nome del prodotto di cui eliminare le vendite. - * @return 0 in caso di successo, 1 se la vendita non è presente, -1 o codice di errore in caso di fallimento. + * @brief Elimina la registrazione delle vendite di un prodotto. + * Lo stock si "ripristina" da solo perche' e' derivato. + * @param productsFile Non usato, mantenuto per compatibilita'. + * @return 0 ok, 1 vendita non presente, -1 errore. */ int deleteSold(const char *productsFile, const char *soldFile, const char *name); /** - * @brief Aggiorna la quantità di copie vendute per un prodotto, regolando di conseguenza il magazzino. - * @param productsFile Percorso del file JSON dei prodotti. - * @param soldFile Percorso del file JSON delle vendite. - * @param name Nome del prodotto. - * @param qty Nuova quantità totale venduta. - * @return 0 in caso di successo, 1 se non presente, codice di errore altrimenti. + * @brief Imposta le copie vendute di un prodotto; se aumentano controlla la disponibilita'. + * @return 0 ok, 1 vendita non presente, 2 scorta insufficiente, -1 errore. */ int updateSold(const char *productsFile, const char *soldFile, const char *name, int qty); /** - * @brief Registra una vendita scalando l'inventario dei prodotti e incrementando le vendite, con supporto di rollback in caso di problemi. - * @param productsFile Percorso del file JSON dei prodotti. - * @param soldFile Percorso del file JSON delle vendite. - * @param name Nome del prodotto venduto. - * @param qty Quantità venduta. - * @return 0 in caso di successo, codice di errore in caso di fallimento. + * @brief Registra una vendita dopo aver verificato la disponibilita' (anche dei componenti, se bundle). + * Scrive solo il file vendite. + * @return 0 ok, 1 prodotto inesistente, 2 scorta insufficiente, -1 errore. */ int recordSale(const char *productsFile, const char *soldFile, const char *name, int qty); @@ -182,10 +141,8 @@ int recordSale(const char *productsFile, const char *soldFile, const char *name, /* ------------------------------------------------------------------ */ /** - * @brief Calcola le entrate totali sommando il prodotto tra prezzo e copie vendute per ciascun articolo. - * @param productsFile Percorso del file JSON dei prodotti. - * @param soldFile Percorso del file JSON delle vendite. - * @return Il totale delle entrate, oppure -1.0 in caso di errore. + * @brief Entrate totali: somma di prezzo * copie vendute. + * @return Il totale, oppure -1.0 in caso di errore. */ double getTotalRevenue(const char *productsFile, const char *soldFile); @@ -194,11 +151,8 @@ double getTotalRevenue(const char *productsFile, const char *soldFile); /* ------------------------------------------------------------------ */ /** - * @brief Esporta i dati delle vendite e dei prodotti in un file in formato CSV. - * @param productsFile Percorso del file JSON dei prodotti. - * @param soldFile Percorso del file JSON delle vendite. - * @param outFile Percorso del file CSV di output. - * @return 0 in caso di successo, un valore negativo in caso di errore. + * @brief Esporta prodotti e vendite in CSV (separatore ';'). + * @return 0 ok, -1 errore di lettura prodotti, -2 errore di apertura del file di destinazione. */ int exportSalesToCsv(const char *productsFile, const char *soldFile, const char *outFile); @@ -207,23 +161,20 @@ int exportSalesToCsv(const char *productsFile, const char *soldFile, const char /* ------------------------------------------------------------------ */ /** - * @brief Assicura che il file di configurazione dell'applicazione esista, creandolo se necessario. - * @param fname Percorso del file di configurazione. - * @return 0 in caso di successo, codice di errore altrimenti. + * @brief Crea il file di configurazione se non esiste. + * @return 0 ok, altro in caso di errore. */ int ensureAppConfig(const char *fname); /** - * @brief Carica la configurazione dell'applicazione dal file specificato. - * @param fname Percorso del file di configurazione. - * @return 0 in caso di successo, -1 in caso di errore. + * @brief Carica la configurazione dal file specificato. + * @return 0 ok, -1 errore. */ int loadAppConfig(const char *fname); /** - * @brief Salva la configurazione corrente dell'applicazione sul file specificato. - * @param fname Percorso del file di configurazione. - * @return 0 in caso di successo, -1 in caso di errore. + * @brief Salva la configurazione corrente sul file specificato. + * @return 0 ok, -1 errore. */ int saveAppConfig(const char *fname); diff --git a/src/logic.c b/src/logic.c index fba8ca8..ef1ccde 100644 --- a/src/logic.c +++ b/src/logic.c @@ -5,150 +5,243 @@ #include #include "../include/logic.h" -#include - #include "../include/globals.h" #include "../include/jsonFile.h" -static int isBundleItem(json_object *item); +/* + * Formato dei file (JSON): + * + * File prodotti: [ {"name": "Maglietta S", "price": 19.90, "initial": 12}, ... ] + * initial = giacenza INIZIALE (intero), oppure -1 = infinito + * bundle = 1, components = [ {"name": "...", "qty": 2}, ... ] + * File vendite: [ {"name": "Maglietta S", "sold": 12}, ... ] + * + * Lo stock corrente NON e' salvato: e' calcolato al volo come + * initial - (vendite dirette + vendite di bundle che lo contengono) + * Quindi vendere / modificare / eliminare una vendita tocca solo il file vendite. + * + * Compilazione: aggiungere -ljson-c (e -lncurses) al linker. + * + * TODO: Aggiungere la possibilita' di registrare gli acquisti non compiuti + * con intenzioni di acquisto. + * e.g. persona vuole acquistare maglietta S ma non c'e' la taglia + */ -static int readInventory(json_object *item, int *stock, int *tracked) { - json_object *jstock = NULL; +#define MAX_BUNDLE_DEPTH 8 - if (!item || !stock || !tracked) - return -1; +/* ------------------------------------------------------------------ */ +/* Helper JSON */ +/* ------------------------------------------------------------------ */ - if (isBundleItem(item)) { - *stock = PRODUCT_STOCK_UNLIMITED; - *tracked = 0; - return 0; - } - - if (!json_object_object_get_ex(item, "stock", &jstock) || !jstock) { - *stock = PRODUCT_STOCK_UNLIMITED; - *tracked = 0; - return 0; - } - - *stock = json_object_get_int(jstock); - if (*stock < 0) { - *stock = PRODUCT_STOCK_UNLIMITED; - *tracked = 0; - } else { - *tracked = 1; - } - - return 0; +static int isBundleItem(json_object *item) { + json_object *v = NULL; + return item && json_object_object_get_ex(item, "bundle", &v) && json_object_get_boolean(v); } -int exportSalesToCsv(const char *productsFile, const char *soldFile, const char *outFile) -{ - json_object *products = getProducts(productsFile); - json_object *sales = getProducts(soldFile); - if (!products || !json_object_is_type(products, json_type_array)) { - if (products) json_object_put(products); - if (sales) json_object_put(sales); - return -1; - } - - FILE *f = fopen(outFile, "w"); - if (!f) { - if (products) json_object_put(products); - if (sales) json_object_put(sales); - return -2; - } - - fprintf(f, "Nome;Inventario iniziale;Vendite;Prezzo;Componenti\n"); - - int n = json_object_array_length(products); - for (int i = 0; i < n; ++i) { - json_object *item = json_object_array_get_idx(products, i); - json_object *jname = NULL; - json_object *jprice = NULL; - json_object *jbundle = NULL; - - const char *name = (json_object_object_get_ex(item, "name", &jname) && jname) ? json_object_get_string(jname) : ""; - double price = (json_object_object_get_ex(item, "price", &jprice) && jprice) ? json_object_get_double(jprice) : 0.0; - int isBundle = (json_object_object_get_ex(item, "bundle", &jbundle) && jbundle) ? json_object_get_int(jbundle) : 0; - - /* sold count */ - int sold = getCount(soldFile, name); - - /* inventory */ - const char *inv_text = NULL; - char inv_buf[64]; - if (isBundle) { - inv_text = "non definito"; - } else { - int stock = -1; - json_object *stock_obj = NULL; - if (json_object_object_get_ex(item, "stock", &stock_obj) && stock_obj) { - stock = json_object_get_int(stock_obj); - } - if (stock < 0) { - inv_text = "non definito"; - } else { - /* initial = current stock + sold */ - snprintf(inv_buf, sizeof(inv_buf), "%d", stock + sold); - inv_text = inv_buf; - } - } - - /* components */ - char comp_buf[512]; - comp_buf[0] = '\0'; - if (isBundle) { - json_object *comp = NULL; - if (json_object_object_get_ex(item, "components", &comp) && comp && json_object_is_type(comp, json_type_array)) { - int m = json_object_array_length(comp); - for (int j = 0; j < m; ++j) { - json_object *c = json_object_array_get_idx(comp, j); - json_object *cjname = NULL; - const char *cname = (json_object_object_get_ex(c, "name", &cjname) && cjname) ? json_object_get_string(cjname) : ""; - if (j) strncat(comp_buf, ";", sizeof(comp_buf)-strlen(comp_buf)-1); - strncat(comp_buf, cname ? cname : "", sizeof(comp_buf)-strlen(comp_buf)-1); - } - } - } - - /* write CSV line; escape semicolons in name/comp if needed (simple) */ - fprintf(f, "%s;%s;%d;%.2f;%s\n", - name ? name : "", - inv_text, - sold, - price, - comp_buf); - } - - fclose(f); - if (products) json_object_put(products); - if (sales) json_object_put(sales); - return 0; -} -static void writeInventory(json_object *item, int stock, int tracked) { - if (!item) - return; - - json_object_object_add(item, "stock", - json_object_new_int(tracked ? stock : PRODUCT_STOCK_UNLIMITED)); +static const char *itemName(json_object *item) { + json_object *v = NULL; + if (item && json_object_object_get_ex(item, "name", &v) && v) + return json_object_get_string(v); + return NULL; } -static int loadProductItem(const char *fname, const char *name, json_object **arr_out, json_object **item_out) { - json_object *arr = loadJsonArray(fname); - if (!arr) - return -1; - - json_object *item = findItemByName(arr, name); - if (!item) { - json_object_put(arr); +static int getComponentQty(json_object *component) { + json_object *v = NULL; + if (!component || !json_object_object_get_ex(component, "qty", &v) || !v) return 1; - } + return json_object_get_int(v); +} - *arr_out = arr; - *item_out = item; +static json_object *getBundleComponents(json_object *bundleItem) { + json_object *components = NULL; + if (!bundleItem || !json_object_object_get_ex(bundleItem, "components", &components) || !components) + return NULL; + if (!json_object_is_type(components, json_type_array)) + return NULL; + return components; +} + +static json_object *findItemByNameExcluding(json_object *arr, const char *name, json_object *exclude) { + if (!arr || !name) + return NULL; + + size_t n = json_object_array_length(arr); + for (size_t i = 0; i < n; i++) { + json_object *item = json_object_array_get_idx(arr, i); + const char *iname = itemName(item); + if (item != exclude && iname && strcmp(iname, name) == 0) + return item; + } + return NULL; +} + +/* File vendite mancante/illeggibile = nessuna vendita. */ +static json_object *loadSalesOrEmpty(const char *fname) { + json_object *arr = loadJsonArray(fname); + return arr ? arr : json_object_new_array(); +} + +/* ------------------------------------------------------------------ */ +/* Calcolo dello stock (sola lettura, nessun effetto collaterale) */ +/* ------------------------------------------------------------------ */ + +static int soldOf(json_object *sales, const char *name) { + json_object *item = findItemByName(sales, name); + json_object *v = NULL; + if (item && json_object_object_get_ex(item, "sold", &v) && v) + return json_object_get_int(v); return 0; } +/* Giacenza iniziale salvata, o PRODUCT_STOCK_UNLIMITED se infinita/bundle. */ +static int getInitial(json_object *item) { + json_object *v = NULL; + if (!item || isBundleItem(item) || !json_object_object_get_ex(item, "initial", &v) || !v) + return PRODUCT_STOCK_UNLIMITED; + int n = json_object_get_int(v); + return (n < 0) ? PRODUCT_STOCK_UNLIMITED : n; +} + +/* + * Quante unita' di "target" consuma UNA vendita di "item". + * - prodotto semplice: 1 se e' il target, altrimenti 0 + * - bundle: somma ricorsiva sui componenti (qty * consumo del componente) + */ +static long unitsNeeded(json_object *products, json_object *item, const char *target, int depth) { + if (!item || depth > MAX_BUNDLE_DEPTH) + return 0; + + if (!isBundleItem(item)) { + const char *n = itemName(item); + return (n && strcmp(n, target) == 0) ? 1 : 0; + } + + json_object *comps = getBundleComponents(item); + if (!comps) + return 0; + + long total = 0; + size_t count = json_object_array_length(comps); + for (size_t i = 0; i < count; i++) { + json_object *c = json_object_array_get_idx(comps, i); + const char *cname = itemName(c); + if (!cname) + continue; + total += (long)getComponentQty(c) * + unitsNeeded(products, findItemByName(products, cname), target, depth + 1); + } + return total; +} + +/* Unita' totali di "target" consumate da tutte le vendite registrate. */ +static long consumedUnits(json_object *products, json_object *sales, const char *target) { + long total = 0; + size_t n = json_object_array_length(sales); + for (size_t i = 0; i < n; i++) { + json_object *sale = json_object_array_get_idx(sales, i); + const char *sname = itemName(sale); + if (!sname) + continue; + json_object *prod = findItemByName(products, sname); + if (!prod) + continue; /* vendita orfana: prodotto non piu' in listino */ + total += (long)soldOf(sales, sname) * unitsNeeded(products, prod, target, 0); + } + return total; +} + +/* Stock corrente di un prodotto semplice, o -1 se infinito. */ +static long currentStock(json_object *products, json_object *sales, json_object *item) { + int initial = getInitial(item); + if (initial < 0) + return -1; + long left = initial - consumedUnits(products, sales, itemName(item)); + return left < 0 ? 0 : left; +} + +/* + * Quante vendite di "item" sono ancora possibili. + * -1 = illimitate. Per un bundle e' il minimo tra le disponibilita' dei componenti. + */ +static long availableUnits(json_object *products, json_object *sales, json_object *item, int depth) { + if (!item || depth > MAX_BUNDLE_DEPTH) + return 0; + + if (!isBundleItem(item)) + return currentStock(products, sales, item); + + json_object *comps = getBundleComponents(item); + if (!comps) + return -1; + + long best = -1; + size_t count = json_object_array_length(comps); + for (size_t i = 0; i < count; i++) { + json_object *c = json_object_array_get_idx(comps, i); + const char *cname = itemName(c); + int q = getComponentQty(c); + if (!cname || q <= 0) + return 0; + + json_object *child = findItemByName(products, cname); + if (!child) + return 0; /* componente sparito: bundle non vendibile */ + + long a = availableUnits(products, sales, child, depth + 1); + if (a < 0) + continue; /* componente illimitato */ + long k = a / q; + if (best < 0 || k < best) + best = k; + } + return best; +} + +/* ------------------------------------------------------------------ */ +/* Scrittura atomica di due file (tmp + rename) */ +/* ------------------------------------------------------------------ */ + +static int writeTmp(const char *fname, json_object *arr, char *tmp, size_t size) { + snprintf(tmp, size, "%s.tmp", fname); + if (json_object_to_file_ext(tmp, arr, JSON_C_TO_STRING_PRETTY) != 0) { + remove(tmp); + return -1; + } + return 0; +} + +/* Scrive prima tutto su file temporanei, poi li rinomina. sales puo' essere NULL. */ +static int commitFiles(const char *productsFile, json_object *products, + const char *soldFile, json_object *sales) { + char tmpP[512], tmpS[512]; + + if (writeTmp(productsFile, products, tmpP, sizeof tmpP) != 0) + return -1; + + if (sales) { + if (writeTmp(soldFile, sales, tmpS, sizeof tmpS) != 0) { + remove(tmpP); + return -1; + } + } + + if (rename(tmpP, productsFile) != 0) { + remove(tmpP); + if (sales) + remove(tmpS); + return -1; + } + if (sales && rename(tmpS, soldFile) != 0) { + remove(tmpS); + return -1; + } + return 0; +} + +/* ------------------------------------------------------------------ */ +/* Configurazione */ +/* ------------------------------------------------------------------ */ + static json_object *buildAppConfigObject(void) { json_object *obj = json_object_new_object(); if (!obj) @@ -181,342 +274,77 @@ static int readConfigString(json_object *obj, const char *key, char *out, size_t return 1; } -static int isBundleItem(json_object *item) { - json_object *v = NULL; - return item && json_object_object_get_ex(item, "bundle", &v) && json_object_get_boolean(v); -} - -static int getComponentQty(json_object *component) { - json_object *v = NULL; - - if (!component) - return 1; - - if (!json_object_object_get_ex(component, "qty", &v) || !v) - return 1; - - return json_object_get_int(v); -} - -static json_object *getBundleComponents(json_object *bundleItem) { - json_object *components = NULL; - - if (!bundleItem) - return NULL; - - if (!json_object_object_get_ex(bundleItem, "components", &components) || !components) - return NULL; - - if (!json_object_is_type(components, json_type_array)) - return NULL; - - return components; -} - -static const char *bundleTagText(json_object *item) { - return isBundleItem(item) ? " [BUNDLE]" : ""; -} - -static void formatProductLabel(json_object *item, char *buf, size_t size) { - json_object *v = NULL; - int tracked = 0; - int stock = PRODUCT_STOCK_UNLIMITED; - - if (!buf || size == 0) - return; - - if (isBundleItem(item)) { - snprintf(buf, size, "X"); - return; - } - - if (json_object_object_get_ex(item, "stock", &v) && v) { - stock = json_object_get_int(v); - tracked = stock >= 0; - } - - if (tracked) - snprintf(buf, size, "%d", stock); - else - snprintf(buf, size, "X"); -} - -static json_object *findItemByNameExcluding(json_object *arr, const char *name, json_object *exclude) { - if (!arr || !name) - return NULL; - - size_t n = json_object_array_length(arr); - for (size_t i = 0; i < n; i++) { - json_object *item = json_object_array_get_idx(arr, i); - json_object *jname = NULL; - - if (item == exclude) - continue; - - if (json_object_object_get_ex(item, "name", &jname) && - strcmp(json_object_get_string(jname), name) == 0) - return item; - } - - return NULL; -} - -static int canConsumeTree(json_object *products, json_object *item, int qty) { - if (!products || !item || qty <= 0) +int saveAppConfig(const char *fname) { + json_object *obj = buildAppConfigObject(); + if (!obj) return -1; - if (!isBundleItem(item)) { - json_object *jstock = NULL; - int stock = PRODUCT_STOCK_UNLIMITED; - - if (!json_object_object_get_ex(item, "stock", &jstock) || !jstock) - return 0; - - stock = json_object_get_int(jstock); - if (stock < 0) - return 0; - - return (stock >= qty) ? 0 : 2; - } - - json_object *components = getBundleComponents(item); - if (!components) - return 0; - - size_t count = json_object_array_length(components); - for (size_t i = 0; i < count; i++) { - json_object *component = json_object_array_get_idx(components, i); - json_object *jname = NULL; - json_object *child = NULL; - int componentQty = getComponentQty(component); - - if (componentQty <= 0) - return -1; - - if (!json_object_object_get_ex(component, "name", &jname) || !jname) - return -1; - - child = findItemByName(products, json_object_get_string(jname)); - if (!child) - return 1; - - int res = canConsumeTree(products, child, qty * componentQty); - if (res != 0) - return res; - } + int res = writeAppConfigObject(fname, obj); + json_object_put(obj); + return res; +} +int ensureAppConfig(const char *fname) { + if (!existsFile(fname)) + return saveAppConfig(fname); return 0; } -static int consumeTree(json_object *products, json_object *item, int qty) { - if (!products || !item || qty <= 0) +int loadAppConfig(const char *fname) { + if (ensureAppConfig(fname) != 0) return -1; - if (!isBundleItem(item)) { - json_object *jstock = NULL; - int stock = PRODUCT_STOCK_UNLIMITED; - - if (!json_object_object_get_ex(item, "stock", &jstock) || !jstock) - return 0; - - stock = json_object_get_int(jstock); - if (stock < 0) - return 0; - - json_object_object_add(item, "stock", json_object_new_int(stock - qty)); - return 0; - } - - json_object *components = getBundleComponents(item); - if (!components) - return 0; - - size_t count = json_object_array_length(components); - for (size_t i = 0; i < count; i++) { - json_object *component = json_object_array_get_idx(components, i); - json_object *jname = NULL; - json_object *child = NULL; - int componentQty = getComponentQty(component); - - if (!json_object_object_get_ex(component, "name", &jname) || !jname) - return -1; - - child = findItemByName(products, json_object_get_string(jname)); - if (!child) - return 1; - - int res = consumeTree(products, child, qty * componentQty); - if (res != 0) - return res; + json_object *loaded = json_object_from_file(fname); + if (!loaded || !json_object_is_type(loaded, json_type_object)) { + if (loaded) + json_object_put(loaded); + return saveAppConfig(fname); } + readConfigString(loaded, "productsFile", productPricesFileLocation, sizeof productPricesFileLocation); + readConfigString(loaded, "salesFile", countFileLocation, sizeof countFileLocation); + json_object_put(loaded); return 0; } -static int restoreTree(json_object *products, json_object *item, int qty) { - if (!products || !item || qty <= 0) return -1; - - if (!isBundleItem(item)) { - json_object *jstock = NULL; - if (!json_object_object_get_ex(item, "stock", &jstock) || !jstock) - return 0; - int stock = json_object_get_int(jstock); - if (stock < 0) return 0; /* non tracciato */ - json_object_object_add(item, "stock", json_object_new_int(stock + qty)); - return 0; - } - - json_object *components = getBundleComponents(item); - if (!components) return 0; - - size_t count = json_object_array_length(components); - for (size_t i = 0; i < count; i++) { - json_object *c = json_object_array_get_idx(components, i); - json_object *jname = NULL; - if (!json_object_object_get_ex(c, "name", &jname) || !jname) return -1; - json_object *child = findItemByName(products, json_object_get_string(jname)); - if (!child) return 1; - int res = restoreTree(products, child, qty * getComponentQty(c)); - if (res != 0) return res; - } - return 0; -} - -int restoreProductInventory(const char *fname, const char *name, int qty) { - json_object *arr = loadJsonArray(fname); - if (!arr) return -1; - json_object *item = findItemByName(arr, name); - if (!item) { json_object_put(arr); return 1; } - int res = restoreTree(arr, item, qty); - if (res == 0) res = saveJsonArray(fname, arr); - json_object_put(arr); - return res == 0 ? 0 : -1; -} - -static char *loadFileText(const char *fname) { - FILE *f = fopen(fname, "rb"); - long size; - char *buf; - - if (!f) - return NULL; - - if (fseek(f, 0, SEEK_END) != 0) { - fclose(f); - return NULL; - } - - size = ftell(f); - if (size < 0) { - fclose(f); - return NULL; - } - - rewind(f); - buf = malloc((size_t)size + 1); - if (!buf) { - fclose(f); - return NULL; - } - - if (size > 0 && fread(buf, 1, (size_t)size, f) != (size_t)size) { - free(buf); - fclose(f); - return NULL; - } - - buf[size] = '\0'; - fclose(f); - return buf; -} - -static int saveFileText(const char *fname, const char *text) { - FILE *f = fopen(fname, "wb"); - size_t len; - - if (!f) - return -1; - - len = strlen(text); - if (len > 0 && fwrite(text, 1, len, f) != len) { - fclose(f); - return -1; - } - - fclose(f); - return 0; -} - -/* - * Formato dei file (JSON): - * - * File prodotti: [ {"name": "Maglietta S", "price": 19.90, "stock": 12}, ... ] - * stock = numero intero, oppure -1 per inventario infinito - * bundle = 1, components = [ {"name": "...", "qty": 2}, ... ] - * File vendite: [ {"name": "Maglietta S", "sold": 12}, ... ] - * - * Compilazione: aggiungere -ljson-c (e -lncurses) al linker. - * - * TODO: Aggiungere la possibilità di registrare gli acquisti non compiuti - * con intenzioni di acquisto. - * e.g. persona vuole acquistare maglietta S ma non c'è la taglia - */ - /* ------------------------------------------------------------------ */ /* Prodotti */ /* ------------------------------------------------------------------ */ -/* - * Ritorna l'array JSON dei prodotti (da liberare con json_object_put()), - * oppure NULL in caso di errore. - */ +/* Array JSON dei prodotti (da liberare con json_object_put()), NULL se errore. */ json_object *getProducts(const char *fname) { return loadJsonArray(fname); } -/* Stampa a schermo (ncurses) la lista dei prodotti con il relativo prezzo. */ -void listProducts(const char *fname) { - json_object *arr = loadJsonArray(fname); - if (!arr) { - printw("Errore nella lettura del file %s\n", fname); - refresh(); - return; - } +/* + * Come getProducts, ma ogni prodotto semplice ha in piu' un campo "stock" + * CALCOLATO (-1 = infinito); i bundle hanno sempre -1. + * E' una vista in memoria: NON va mai salvata su file. + * Cosi' la UI che legge "stock" continua a funzionare senza modifiche. + */ +json_object *getProductsWithStock(const char *productsFile, const char *soldFile) { + json_object *products = loadJsonArray(productsFile); + if (!products) + return NULL; - size_t n = json_object_array_length(arr); - if (n == 0) - printw("Nessun prodotto presente.\n"); + json_object *sales = loadSalesOrEmpty(soldFile); + size_t n = json_object_array_length(products); for (size_t i = 0; i < n; i++) { - json_object *item = json_object_array_get_idx(arr, i); - json_object *jname, *jprice; - char stockBuf[16]; - - if (json_object_object_get_ex(item, "name", &jname) && - json_object_object_get_ex(item, "price", &jprice)) - formatProductLabel(item, stockBuf, sizeof stockBuf); - - if (json_object_object_get_ex(item, "name", &jname) && - json_object_object_get_ex(item, "price", &jprice)) - printw("%zu) %-30s %8.2f [%s]%s\n", i + 1, - json_object_get_string(jname), - json_object_get_double(jprice), - stockBuf, - bundleTagText(item)); + json_object *item = json_object_array_get_idx(products, i); + long st = isBundleItem(item) ? -1 : currentStock(products, sales, item); + json_object_object_add(item, "stock", json_object_new_int((int)st)); } - refresh(); - json_object_put(arr); + json_object_put(sales); + return products; } -/* - * Aggiunge un prodotto con il suo prezzo. - * Ritorna 0 se ok, -1 se errore, 1 se il prodotto esiste già. - */ int addProduct(const char *fname, const char *name, double price) { return addProductWithInventory(fname, name, price, PRODUCT_STOCK_UNLIMITED); } +/* stock = giacenza di partenza (negativo = infinita). 0 ok, 1 esiste gia', -1 errore. */ int addProductWithInventory(const char *fname, const char *name, double price, int stock) { json_object *arr = loadJsonArray(fname); if (!arr) @@ -530,7 +358,8 @@ int addProductWithInventory(const char *fname, const char *name, double price, i json_object *item = json_object_new_object(); json_object_object_add(item, "name", json_object_new_string(name)); json_object_object_add(item, "price", json_object_new_double(price)); - writeInventory(item, stock, stock != PRODUCT_STOCK_UNLIMITED); + json_object_object_add(item, "initial", + json_object_new_int(stock < 0 ? PRODUCT_STOCK_UNLIMITED : stock)); json_object_array_add(arr, item); int res = saveJsonArray(fname, arr); @@ -548,7 +377,8 @@ int addBundleProduct(const char *fname, const char *name, double price, json_obj return 1; } - if (!components || !json_object_is_type(components, json_type_array) || json_object_array_length(components) == 0) { + if (!components || !json_object_is_type(components, json_type_array) || + json_object_array_length(components) == 0) { json_object_put(arr); return -1; } @@ -557,7 +387,6 @@ int addBundleProduct(const char *fname, const char *name, double price, json_obj json_object_object_add(item, "name", json_object_new_string(name)); json_object_object_add(item, "price", json_object_new_double(price)); json_object_object_add(item, "bundle", json_object_new_int(1)); - json_object_object_add(item, "stock", json_object_new_int(PRODUCT_STOCK_UNLIMITED)); json_object_object_add(item, "components", components); json_object_array_add(arr, item); @@ -583,23 +412,50 @@ double getPrice(const char *fname, const char *name) { return price; } -int updateProduct(const char *productsFile, const char *soldFile, const char *oldName, const char *newName, double price) { - json_object *products = NULL; - json_object *item = NULL; - int stock = PRODUCT_STOCK_UNLIMITED; - int tracked = 0; - - if (loadProductItem(productsFile, oldName, &products, &item) != 0) +/* + * Stock corrente di un prodotto (calcolato). + * 0 ok, 1 prodotto non trovato, -1 errore. + */ +int getProductInventory(const char *productsFile, const char *soldFile, const char *name, + int *stock, int *tracked) { + if (!stock || !tracked) return -1; - readInventory(item, &stock, &tracked); - json_object_put(products); + json_object *products = loadJsonArray(productsFile); + if (!products) + return -1; - return updateProductWithInventory(productsFile, soldFile, oldName, newName, price, - tracked ? stock : PRODUCT_STOCK_UNLIMITED); + json_object *item = findItemByName(products, name); + if (!item) { + json_object_put(products); + return 1; + } + + if (isBundleItem(item)) { + *stock = PRODUCT_STOCK_UNLIMITED; + *tracked = 0; + } else { + json_object *sales = loadSalesOrEmpty(soldFile); + long st = currentStock(products, sales, item); + json_object_put(sales); + *tracked = (st >= 0); + *stock = (st >= 0) ? (int)st : PRODUCT_STOCK_UNLIMITED; + } + + json_object_put(products); + return 0; } -int updateProductWithInventory(const char *productsFile, const char *soldFile, const char *oldName, const char *newName, double price, int stock) { +/* + * Modifica un prodotto. "stock" e' la giacenza ATTUALE desiderata + * (negativo = infinita): internamente diventa initial = stock + gia' consumato. + * Se il nome cambia, aggiorna nella stessa operazione file vendite e + * riferimenti nei bundle. + * 0 ok, 1 non trovato, 2 nome gia' usato, -1 errore. + */ +int updateProductWithInventory(const char *productsFile, const char *soldFile, + const char *oldName, const char *newName, + double price, int stock) { json_object *products = loadJsonArray(productsFile); if (!products) return -1; @@ -610,101 +466,61 @@ int updateProductWithInventory(const char *productsFile, const char *soldFile, c return 1; } - if (strcmp(oldName, newName) != 0 && findItemByNameExcluding(products, newName, item)) { + int renamed = strcmp(oldName, newName) != 0; + if (renamed && findItemByNameExcluding(products, newName, item)) { json_object_put(products); return 2; } - json_object_object_add(item, "name", json_object_new_string(newName)); + json_object *sales = loadSalesOrEmpty(soldFile); + + /* va calcolato PRIMA di rinominare */ + if (!isBundleItem(item)) { + int initial = (stock < 0) ? PRODUCT_STOCK_UNLIMITED + : (int)(stock + consumedUnits(products, sales, oldName)); + json_object_object_add(item, "initial", json_object_new_int(initial)); + } + json_object_object_del(item, "stock"); /* residuo del vecchio formato */ json_object_object_add(item, "price", json_object_new_double(price)); - writeInventory(item, stock, stock != PRODUCT_STOCK_UNLIMITED); - if (saveJsonArray(productsFile, products) != 0) { - json_object_put(products); - return -1; + if (renamed) { + json_object_object_add(item, "name", json_object_new_string(newName)); + + /* riferimenti nei bundle */ + size_t n = json_object_array_length(products); + for (size_t i = 0; i < n; i++) { + json_object *comps = getBundleComponents(json_object_array_get_idx(products, i)); + if (!comps) + continue; + size_t m = json_object_array_length(comps); + for (size_t j = 0; j < m; j++) { + json_object *c = json_object_array_get_idx(comps, j); + const char *cn = itemName(c); + if (cn && strcmp(cn, oldName) == 0) + json_object_object_add(c, "name", json_object_new_string(newName)); + } + } + + /* vendite (se non ce ne sono, niente da rinominare: non e' un errore) */ + json_object *sitem = findItemByName(sales, oldName); + if (sitem) + json_object_object_add(sitem, "name", json_object_new_string(newName)); } + + int res = commitFiles(productsFile, products, renamed ? soldFile : NULL, renamed ? sales : NULL); json_object_put(products); - - if (strcmp(oldName, newName) != 0) { - json_object *sales = loadJsonArray(soldFile); - if (!sales) - return -1; - - if (renameItemByName(sales, oldName, newName) != 0) { - json_object_put(sales); - return 1; - } - - if (saveJsonArray(soldFile, sales) != 0) { - json_object_put(sales); - return -1; - } - json_object_put(sales); - } - - return 0; -} - -int getProductInventory(const char *fname, const char *name, int *stock, int *tracked) { - json_object *arr = loadJsonArray(fname); - if (!arr) - return -1; - - json_object *item = findItemByName(arr, name); - if (!item) { - json_object_put(arr); - return 1; - } - - int res = readInventory(item, stock, tracked); - json_object_put(arr); - return res; -} - -int setProductInventory(const char *fname, const char *name, int stock, int tracked) { - json_object *arr = loadJsonArray(fname); - if (!arr) - return -1; - - json_object *item = findItemByName(arr, name); - if (!item) { - json_object_put(arr); - return 1; - } - - writeInventory(item, stock, tracked); - - int res = saveJsonArray(fname, arr); - json_object_put(arr); + json_object_put(sales); return (res == 0) ? 0 : -1; } -int consumeProductInventory(const char *fname, const char *name, int qty) { - json_object *arr = loadJsonArray(fname); - if (!arr) - return -1; - - json_object *item = findItemByName(arr, name); - if (!item) { - json_object_put(arr); - return 1; - } - - int check = canConsumeTree(arr, item, qty); - if (check != 0) { - json_object_put(arr); - return check; - } - - int res = consumeTree(arr, item, qty); - if (res != 0) { - json_object_put(arr); - return res; - } - - res = saveJsonArray(fname, arr); - json_object_put(arr); - return (res == 0) ? 0 : -1; +int updateProduct(const char *productsFile, const char *soldFile, + const char *oldName, const char *newName, double price) { + int stock = PRODUCT_STOCK_UNLIMITED, tracked = 0; + int r = getProductInventory(productsFile, soldFile, oldName, &stock, &tracked); + if (r != 0) + return r; + return updateProductWithInventory(productsFile, soldFile, oldName, newName, price, + tracked ? stock : PRODUCT_STOCK_UNLIMITED); } /* ------------------------------------------------------------------ */ @@ -717,13 +533,7 @@ int getCount(const char *fname, const char *name) { if (!arr) return -1; - int count = 0; - json_object *item = findItemByName(arr, name); - json_object *jsold; - - if (item && json_object_object_get_ex(item, "sold", &jsold)) - count = json_object_get_int(jsold); - + int count = soldOf(arr, name); json_object_put(arr); return count; } @@ -756,11 +566,7 @@ void listSold(const char *fname) { json_object_put(arr); } -/* - * Aggiunge "qty" copie vendute al prodotto indicato - * (crea la voce se non esiste ancora). - * Ritorna 0 se ok, -1 se errore. - */ +/* Aggiunge "qty" copie vendute (crea la voce se non esiste). 0 ok, -1 errore. */ int addSold(const char *fname, const char *name, int qty) { json_object *arr = loadJsonArray(fname); if (!arr) @@ -769,13 +575,7 @@ int addSold(const char *fname, const char *name, int qty) { json_object *item = findItemByName(arr, name); if (item) { - json_object *jsold; - int current = 0; - - if (json_object_object_get_ex(item, "sold", &jsold)) - current = json_object_get_int(jsold); - - json_object_object_add(item, "sold", json_object_new_int(current + qty)); + json_object_object_add(item, "sold", json_object_new_int(soldOf(arr, name) + qty)); } else { item = json_object_new_object(); json_object_object_add(item, "name", json_object_new_string(name)); @@ -788,86 +588,68 @@ int addSold(const char *fname, const char *name, int qty) { return (res == 0) ? 0 : -1; } +/* + * Registra una vendita. Scrive UN solo file (vendite). + * 0 ok, 1 prodotto inesistente, 2 scorta insufficiente, -1 errore. + */ int recordSale(const char *productsFile, const char *soldFile, const char *name, int qty) { - char *backup = NULL; - int consumeRes; - if (qty <= 0) return -1; if (!existsFile(productsFile) && createFile(productsFile) != 0) return -1; - - backup = loadFileText(productsFile); - if (!backup) + if (!existsFile(soldFile) && createFile(soldFile) != 0) return -1; - consumeRes = consumeProductInventory(productsFile, name, qty); - if (consumeRes != 0) { - free(backup); - return consumeRes; - } - - if (addSold(soldFile, name, qty) != 0) { - saveFileText(productsFile, backup); - free(backup); + json_object *products = loadJsonArray(productsFile); + if (!products) return -1; + + json_object *item = findItemByName(products, name); + if (!item) { + json_object_put(products); + return 1; } - free(backup); - return 0; + json_object *sales = loadSalesOrEmpty(soldFile); + long avail = availableUnits(products, sales, item, 0); + json_object_put(sales); + json_object_put(products); + + if (avail >= 0 && qty > avail) + return 2; + + return addSold(soldFile, name, qty) == 0 ? 0 : -1; } +/* + * Elimina una vendita. Lo stock si "ripristina" da solo (e' derivato). + * productsFile resta nella firma per compatibilita' con la UI. + * 0 ok, 1 vendita non presente, -1 errore. + */ int deleteSold(const char *productsFile, const char *soldFile, const char *name) { + (void)productsFile; + json_object *sales = loadJsonArray(soldFile); if (!sales) return -1; - json_object *item = findItemByName(sales, name); - if (!item) { + if (!findItemByName(sales, name)) { json_object_put(sales); - return 1; /* vendita non presente */ - } - - json_object *jsold = NULL; - int qty = 0; - if (json_object_object_get_ex(item, "sold", &jsold) && jsold) - qty = json_object_get_int(jsold); - - /* backup dei prodotti per il rollback */ - char *backup = NULL; - if (qty > 0) { - backup = loadFileText(productsFile); - if (!backup) { - json_object_put(sales); - return -1; - } - - int res = restoreProductInventory(productsFile, name, qty); - /* 1 = prodotto non più presente: niente da ripristinare, si procede */ - if (res != 0 && res != 1) { - free(backup); - json_object_put(sales); - return res; - } + return 1; } int res = removeItemByName(sales, name); if (res == 0) res = saveJsonArray(soldFile, sales); json_object_put(sales); - - if (res != 0) { - if (backup) - saveFileText(productsFile, backup); /* rollback inventario */ - free(backup); - return (res < 0) ? res : -1; - } - - free(backup); - return 0; + return (res == 0) ? 0 : -1; } +/* + * Imposta le copie vendute a "qty". Se aumentano, controlla la disponibilita'. + * 0 ok, 1 vendita non presente, 2 scorta insufficiente, -1 errore. + */ int updateSold(const char *productsFile, const char *soldFile, const char *name, int qty) { if (qty < 0) return -1; @@ -879,60 +661,40 @@ int updateSold(const char *productsFile, const char *soldFile, const char *name, json_object *item = findItemByName(sales, name); if (!item) { json_object_put(sales); - return 1; /* vendita non presente */ + return 1; } - json_object *jsold = NULL; - int oldQty = 0; - if (json_object_object_get_ex(item, "sold", &jsold) && jsold) - oldQty = json_object_get_int(jsold); + int delta = qty - soldOf(sales, name); - int delta = qty - oldQty; /* >0: vendute di più, <0: vendute di meno */ - char *backup = NULL; - - if (delta != 0) { - backup = loadFileText(productsFile); - if (!backup) { + if (delta > 0) { + json_object *products = loadJsonArray(productsFile); + if (!products) { json_object_put(sales); return -1; } - - int res; - if (delta > 0) - res = consumeProductInventory(productsFile, name, delta); /* controlla anche la disponibilità */ - else - res = restoreProductInventory(productsFile, name, -delta); - - if (res != 0 && !(delta < 0 && res == 1)) { - free(backup); - json_object_put(sales); - return (res == 1) ? -1 : res; /* evita ambiguità con "vendita non presente" */ + json_object *prod = findItemByName(products, name); + if (prod) { /* se il prodotto non esiste piu' non c'e' stock da controllare */ + long avail = availableUnits(products, sales, prod, 0); + if (avail >= 0 && delta > avail) { + json_object_put(products); + json_object_put(sales); + return 2; + } } + json_object_put(products); } json_object_object_add(item, "sold", json_object_new_int(qty)); int res = saveJsonArray(soldFile, sales); json_object_put(sales); - - if (res != 0) { - if (backup) - saveFileText(productsFile, backup); /* rollback inventario */ - free(backup); - return -1; - } - - free(backup); - return 0; + return (res == 0) ? 0 : -1; } /* ------------------------------------------------------------------ */ /* Entrate */ /* ------------------------------------------------------------------ */ -/* - * Calcola le entrate totali: somma di (prezzo * copie vendute) - * per ogni prodotto. Ritorna -1.0 in caso di errore. - */ +/* Somma di (prezzo * copie vendute). Ritorna -1.0 in caso di errore. */ double getTotalRevenue(const char *productsFile, const char *soldFile) { json_object *products = loadJsonArray(productsFile); if (!products) @@ -965,45 +727,87 @@ double getTotalRevenue(const char *productsFile, const char *soldFile) { return total; } -int saveAppConfig(const char *fname) { - json_object *obj = buildAppConfigObject(); - if (!obj) - return -1; +/* ------------------------------------------------------------------ */ +/* Export CSV */ +/* ------------------------------------------------------------------ */ - int res = writeAppConfigObject(fname, obj); - json_object_put(obj); - return res; +static void csvField(FILE *f, const char *s) { + if (!s) + s = ""; + if (!strpbrk(s, ";\"\n")) { + fputs(s, f); + return; + } + fputc('"', f); + for (; *s; s++) { + if (*s == '"') + fputc('"', f); + fputc(*s, f); + } + fputc('"', f); } -int ensureAppConfig(const char *fname) { - if (!existsFile(fname)) - return saveAppConfig(fname); - return 0; -} - -int loadAppConfig(const char *fname) { - json_object *obj = NULL; - json_object *loaded = NULL; - - if (ensureAppConfig(fname) != 0) +/* 0 ok, -1 errore lettura prodotti, -2 errore apertura file di destinazione. */ +int exportSalesToCsv(const char *productsFile, const char *soldFile, const char *outFile) { + json_object *products = getProducts(productsFile); + if (!products || !json_object_is_type(products, json_type_array)) { + if (products) + json_object_put(products); return -1; - - loaded = json_object_from_file(fname); - if (!loaded || !json_object_is_type(loaded, json_type_object)) { - if (loaded) - json_object_put(loaded); - return saveAppConfig(fname); } - readConfigString(loaded, "productsFile", productPricesFileLocation, sizeof productPricesFileLocation); - readConfigString(loaded, "salesFile", countFileLocation, sizeof countFileLocation); + json_object *sales = loadSalesOrEmpty(soldFile); /* letto UNA volta sola */ - obj = buildAppConfigObject(); - if (!obj) { - json_object_put(loaded); - return -1; + FILE *f = fopen(outFile, "w"); + if (!f) { + json_object_put(products); + json_object_put(sales); + return -2; } - json_object_put(obj); - json_object_put(loaded); + + fprintf(f, "Nome;Inventario iniziale;Vendite;Prezzo;Componenti\n"); + + size_t n = json_object_array_length(products); + for (size_t i = 0; i < n; i++) { + json_object *item = json_object_array_get_idx(products, i); + json_object *jprice = NULL; + const char *name = itemName(item); + double price = (json_object_object_get_ex(item, "price", &jprice) && jprice) + ? json_object_get_double(jprice) : 0.0; + int sold = name ? soldOf(sales, name) : 0; + + char inv[32]; + int initial = getInitial(item); + if (initial < 0) + snprintf(inv, sizeof inv, "non definito"); + else + snprintf(inv, sizeof inv, "%d", initial); + + /* componenti separati da " | " (non da ';', che e' il separatore del CSV) */ + char comps[512]; + comps[0] = '\0'; + json_object *clist = getBundleComponents(item); + if (isBundleItem(item) && clist) { + size_t off = 0; + size_t m = json_object_array_length(clist); + for (size_t j = 0; j < m && off < sizeof comps; j++) { + json_object *c = json_object_array_get_idx(clist, j); + int w = snprintf(comps + off, sizeof comps - off, "%s%s x%d", + j ? " | " : "", itemName(c) ? itemName(c) : "", getComponentQty(c)); + if (w < 0) + break; + off += (size_t)w; + } + } + + csvField(f, name); + fprintf(f, ";%s;%d;%.2f;", inv, sold, price); + csvField(f, comps); + fputc('\n', f); + } + + fclose(f); + json_object_put(products); + json_object_put(sales); return 0; } diff --git a/src/windowsDeclaration.c b/src/windowsDeclaration.c index bb71092..bb82fb3 100644 --- a/src/windowsDeclaration.c +++ b/src/windowsDeclaration.c @@ -764,7 +764,7 @@ static void showCreateBundleWindow(void) { return; } - json_object *products = getProducts(productPricesFileLocation); + json_object *products = getProductsWithStock(productPricesFileLocation, countFileLocation); if (!products) { warningf("Impossibile leggere il file '%s'.", productPricesFileLocation); return; @@ -927,7 +927,7 @@ void runAboutWindow() { WINDOW *aboutWin; aboutWin = create_newwin(LINES / 2 - 2, COLS / 2, LINES / 2, 0); - wwrap_print(aboutWin, 1, 1, "Questo e' un software fatto per Roma2LUG, il codice verra' pubblicato sul gitea del lug in futuro. La parte logica e' fatta con Claude Code, non avevo voglia di scrivere quei pezzi e volevo effettivamente rendere utile il codice che avevo scritto per gestire ncurses. Oh well, spero vi possa essere comunque utile. Buon lavoro / Divertimento!"); + wwrap_print(aboutWin, 1, 1, "Questo e' un software fatto per Roma2LUG, il codice verra' pubblicato sul gitea del lug in futuro.\nLa parte logica e' fatta con Claude Code, non avevo voglia di scrivere quei pezzi e volevo effettivamente rendere utile il codice che avevo scritto per gestire ncurses.\nOh well, spero vi possa essere comunque utile.\nBuon lavoro / Divertimento!"); statusf("Uovo di pasqua ()"); wrefresh(aboutWin); getch(); @@ -976,7 +976,7 @@ void runOptionsMenu() { /* ------------------------------------------------------------------ */ void runSellWindow() { - json_object *products = getProducts(productPricesFileLocation); + json_object *products = getProductsWithStock(productPricesFileLocation, countFileLocation); if (!products) { warningf("Impossibile leggere il file '%s'.", productPricesFileLocation); return; @@ -1031,7 +1031,7 @@ void runSellWindow() { if (res == 0) { int stock = 0; int tracked = 0; - if (getProductInventory(productPricesFileLocation, name, &stock, &tracked) == 0 && tracked) + if (getProductInventory(productPricesFileLocation, countFileLocation, name, &stock, &tracked) == 0 && tracked) statusf("Registrata vendita: %d x '%s' = %.2f (rimasti: %d)", qty, name, qty * price, stock); else statusf("Registrata vendita: %d x '%s' = %.2f", qty, name, qty * price); @@ -1103,7 +1103,7 @@ void runViewSalesWindow() { void runViewProductsWindow() { while (1) { - json_object *products = getProducts(productPricesFileLocation); + json_object *products = getProductsWithStock(productPricesFileLocation, countFileLocation); if (!products) { warningf("Impossibile leggere il file '%s'.", productPricesFileLocation); return;