FunkyJuiceRecipes/SPECS.md

333 lines
12 KiB
Markdown
Raw Normal View History

2025-12-18 15:49:31 +00:00
# FunkyJuice Recipes
A python application for managing e-juice recipes
* Uses SQLite3 as database
* Uses Toga as GUI
* Uses Peewee ORM
* Separate files per model
* Separate files per migration
* Migrations for database schema changes using `playhouse.migrate`
* Migrations are versioned via the date they were created (YYYY-MM-DD_migration_name.py)
* Migrations state is stored in a Migrations table in the database
* Migrations table does not use a migration file of its own
* Migrations are run automatically on startup (check migration files vs database entries in migrations table)
* At startup
* The app creates (if missing) the peruser data directory at `app.paths.data`
* The app stores the SQLite DB at `<app.paths.data>/funkyjuicerecipes.data`.
* The connection enables pragmas: `{'foreign_keys': 1, 'journal_mode': 'wal', 'synchronous': 1}`.
* Tests use a temporary DB path.
* Uses class wrappers for Toga widgets
* FunkyJuiceRecipesApp(toga.App)
* MainWindow(toga.MainWindow)
* RecipesTable(toga.Table)
* RecipeForm(toga.Box -> with form elements as needed)
* AddRecipeButton(toga.Button)
* EditRecipeButton(toga.Button)
* DeleteRecipeButton(toga.Button -> with confirmation dialog)
* FlavorsTable(toga.Table)
* AddFlavorButton(toga.Button)
* EditFlavorButton(toga.Button)
* DeleteFlavorButton(toga.Button -> with confirmation dialog)
* FlavorForm(toga.Box -> with form elements as needed)
* Uses briefcase to package app for distribution
* Code follows PEP8
* Code follows SOLID principles
* SRP at method level
## How to Run
* `pip install briefcase`
* or `uv add briefcase`
* `briefcase dev`
* or `python -m funkyjuicerecipes.app`
## Flavors Soft Delete Policy
* When a flavor is deleted
* Flavors are Soft Deleted if they are in a recipe.
* Flavors are Hard Deleted if they are not in a recipe.
* Flavors can be Un-Deleted when viewing the Soft Deleted Flavors list.
* Flavors can be Hard Deleted when viewing the Soft Deleted List and they are no longer in a recipe.
* UI Queries default to `WHERE is_deleted = 0`
* Hard Delete is only allowed when `NOT EXISTS (SELECT 1 FROM recipe_flavor WHERE flavor_id=flavor.id)`
## Directory/File Structure
* project-root/
* pyproject.toml
* README.md
* src/
* funkyjuicerecipes/
* __init__.py
* app.py # Toga App subclass and `main()`
* ui/
* __init__.py
* buttons/
* __init__.py
* add_recipe_button.py
* edit_recipe_button.py
* delete_recipe_button.py
* add_flavor_button.py
* edit_flavor_button.py
* delete_flavor_button.py
* forms/
* __init__.py
* recipe_form.py
* flavor_form.py
* tables/
* __init__.py
* recipes_table.py
* flavors_table.py
* dialogs/
* __init__.py
* delete_recipe_confirmation.py
* delete_flavor_confirmation.py
* data/
* __init__.py
* db.py # database init (Peewee SqliteDatabase, pragmas)
* models/
* __init__.py
* flavor.py
* recipe.py
* recipe_flavor.py
* migrations/
* __init__.py
* 2025-12-18_flavor_table_created.py
* 2025-12-18_recipe_table_created.py
* 2025-12-18_recipe_flavor_table_created.py
* logic/
* __init__.py
* calculations.py # pure functions for PG/VG/nic/flavor math
* resources/ # icons, etc.
* tests/
* test_calculations.py
## Requirements
* Python 3.8+
* Toga
* Peewee
* playhouse.migrate
* Briefcase
## DB Schema
* Enable FKs on connection: `SqliteDatabase(path, pragmas={'foreign_keys': 1})`
* Flavor Table
```
flavor(
id INTEGER PRIMARY KEY,
name TEXT COLLATE NOCASE NOT NULL,
company TEXT COLLATE NOCASE NOT NULL,
base TEXT CHECK(base IN ('PG','VG')) NOT NULL,
is_deleted INTEGER NOT NULL DEFAULT 0,
deleted_at DATETIME NULL,
UNIQUE(name, company)
)
CREATE INDEX IF NOT EXISTS idx_flavor_name_company ON flavor(name, company);
```
* Recipe Table
```
recipe(
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
size_ml INTEGER NOT NULL,
base_pg_pct REAL NOT NULL,
base_vg_pct REAL NOT NULL,
nic_pct REAL CHECK(nic_pct >= 0 AND nic_pct <= 100) NOT NULL,
nic_base TEXT CHECK(nic_base IN ('PG','VG')) NOT NULL,
CHECK (
base_pg_pct >= 0 AND
base_vg_pct >= 0 AND
base_pg_pct + base_vg_pct = 100
)
)
```
* RecipeFlavor XREF Table
```
recipe_flavor(
id INTEGER PRIMARY KEY,
recipe_id INTEGER NOT NULL,
flavor_id INTEGER NOT NULL,
pct REAL CHECK(pct >= 0 AND pct <= 100) NOT NULL,
FOREIGN KEY(recipe_id)
REFERENCES recipe(id) ON DELETE CASCADE,
FOREIGN KEY(flavor_id) REFERENCES flavor(id) ON DELETE RESTRICT,
UNIQUE(recipe_id, flavor_id)
)
CREATE INDEX IF NOT EXISTS idx_recipe_flavor_recipe ON recipe_flavor(recipe_id);
CREATE INDEX IF NOT EXISTS idx_recipe_flavor_flavor ON recipe_flavor(flavor_id);
```
* Migrations Table
```
migrations(
id INTEGER PRIMARY KEY,
batch INTEGER NOT NULL,
filename TEXT NOT NULL,
date_run DATETIME NOT NULL,
UNIQUE(filename)
)
```
## Recipes
* Recipes require
* Name,
* Size (ml),
* PG Base %,
* VG base %,
* Nic Base (PG/VG)
* Nic %,
* Flavor List
* Recipe equations are:
* flavor_pg_pct = sum(pct for flavor in flavors if flavor.base == 'PG')
* flavor_vg_pct = sum(pct for flavor in flavors if flavor.base == 'VG')
* pg_pct = base_pg_pct - flavor_pg_pct - (nic_pct if nic_base == 'PG' else 0)
* vg_pct = base_vg_pct - flavor_vg_pct - (nic_pct if nic_base == 'VG' else 0)
* PG_ml = pg_pct / 100 * size_ml
* VG_ml = vg_pct / 100 * size_ml
* Nic_ml = nic_pct / 100 * size_ml
* Flavor_i_ml = flavor_i.pct / 100 * size_ml
* Recipe output is:
* Recipe Name
* Size (ml)
* PG: PG_ml
* VG: VG_ml
* Nic: Nic_ml
* Flavors
* If viewing individual flavors:
* Flavor 1 Name: Flavor_1_ml
* Flavor 2 Name: Flavor_2_ml
* if viewing Flavor Prep:
* "Flavor Prep": Flavor_Prep_ml (sum(pct for flavor in flavors)/100 * size_ml)
## Main Screen
* Menu Bar with File, Recipes, and Help
* File Menu
* New Recipe
* Exit
* Recipes Menu
* Shows a list of saved recipes as menu items
* Selecting menu item opens the recipe screen
* Help Menu
* About screen
* Shows Saved Recipes List
* Recipe Name
* Button to open the recipe screen (View only)
* Button to edit the recipe (Edit form)
* Button to delete the recipe (Confirmation dialog)
* Has button on top to open Inventory Screen.
## About Screen
* Shows
* Application Name (FunkyJuice Recipes)
* Version Number
* Author (Funky Waddle)
## View Recipe Screen
* Needs access to soft deleted flavors for recipes that currently use deleted flavors.
* Show components rounded to 1 decimal point
* Just convert percentage to decimal number and display, no need to validate sum(amounts) = Size (ml)
* We store the percent in the database, not the actual amount, so there is no check that the amount adds to size (ml), only that sum(percentages) = 100
* Shows recipe name,
* Amount in ml,
* PG: PG_ml, (rounded to 1 decimal point)
* VG: VG_ml, (rounded to 1 decimal point)
* Nic: Nic_ml, (rounded to 1 decimal point)
* Flavors
* If viewing individual flavors:
* Flavor 1 Name: Flavor_1_ml (rounded to 1 decimal point)
* Flavor 2 Name: Flavor_2_ml (rounded to 1 decimal point)
* if viewing Flavor Prep:
* Flavor Prep Amount
* Has button to open Edit Recipe Screen
* Has button to open Delete Recipe
* Has button to open Inventory Screen
* Has Drop Down for "View Recipe Using"
* Options are "Individual Flavors" or "Flavor Prep"
* When Flavor Prep is chosen, it replaces the list of flavors with a total ml of the sum of all the flavors in the recipe.
* When Individual Flavors is chosen (default), it replaces the Flavor Prep with the list of individual flavors.
* Has button for "Mix Flavor Prep"
* Flavor Prep is not stored in database. It's calculated on the fly based on flavors in the recipe.
* Shows recipe of Flavors only, no PG, VG, or Nicotine
* Flavor Prep is a batch of flavors
* Take ratio of each flavor to get total amount of each flavor (12% flavor 1 + 2% flavor 2 = 6:1 ratio)
* Flavor Prep batches of different sizes (10ml, 30ml, 120ml)
* Flavor Prep is just pre-mixing flavors, much like premixing flour and sugar for use in cookie batter.
* You can make 10ml or 1000ml of Sugar Flour, and it won't change the amount used in the cookie recipe.
* You can pre-mix 1000ml of flavors, and use 10ml per recipe, giving you 100 batches of Flavor Prep.
## Edit Recipe Screen
* Needs access to soft deleted flavors for recipes that currently use deleted flavors.
* Has a form for editing the recipe
* Name
* Size (mL) (10ml, 30ml, 120ml)
* PG Base %
* VG Base %
* Nic Base (PG or VG)
* Nic %
* Flavors
* Name (with company)
* Amount %
* If recipe contains a deleted flavor,
* Recipe can be saved with the deleted flavor still in the list, as long as it was not changed.
* Cannot actively change a flavor to a deleted flavor.
* Has button to save changes (Save form, then return to View Recipe screen)
* Has button to cancel changes (Discard form changes, return to View Recipe screen)
* Has button to delete recipe (Confirmation dialog)
* On Save, if recipe contains duplicate flavors,
* If duplicate flavor is found:
* Show question dialog to ask if user wants to "Combine the duplicate flavors" or "Cancel Save" and return to Edit Recipe screen.
* If user chooses to combine duplicate flavors,
* Combine flavors with same name and company into one flavor with combined percentage.
* If user chooses to cancel save,
* DO NOT SAVE and close dialog box, returning user to Edit Recipe screen.
* Validate that (PG Base + VG Base) === 100
* Validate that neither PG Base nor VG Base is < 0
* Validate sum(flavor.pct) + nic.pct + pg.pct + vg.pct ~= 100
* On App Exit, if on Edit Recipe screen and data has changed, ask to save changes before exiting.
* On Screen Change, if on Edit Recipe screen and data has changed, ask to save changes before changing screens.
## Add Recipe Screen
* Has a form for adding a recipe
* Name
* Size (mL) (10ml, 30ml, 120ml)
* PG Base %
* VG Base %
* Nic Base (PG or VG)
* Nic %
* Flavors
* Name
* Amount %
* Has button to save changes (Save form, then return to View Recipe screen)
* Has button to cancel changes (Discard form changes, return to View Recipe screen)
* On Save, if recipe contains duplicate flavors,
* If duplicate flavor is found:
* Show question dialog to ask if user wants to "Combine the duplicate flavors" or "Cancel Save" and return to Edit Recipe screen.
* If user chooses to combine duplicate flavors,
* Combine flavors with same name and company into one flavor with combined percentage.
* If user chooses to cancel save,
* DO NOT SAVE and close dialog box, returning user to Edit Recipe screen.
* Validate that (PG Base + VG Base) === 100
* Validate that neither PG Base nor VG Base is < 0
* Validate sum(flavor.pct) + nic.pct + pg.pct + vg.pct ~= 100
* On App Exit, if on Edit Recipe screen and data has changed, ask to save changes before exiting.
* On Screen Change, if on Edit Recipe screen and data has changed, ask to save changes before changing screens.
## Recipe Form Screens (Add/Edit)
* When adding/editing a recipe, you enter the PG, VG, Flavors, and Nic as %
* PG and VG are entered as % of total recipe size
* When changing PG or VG, the other is updated automatically so that PG + VG is always 100%
* Neither PG or VG can be negative
## Inventory Screen
* List of all Non-Deleted Flavors saved in the database
* Flavor Name
* Company
* PG or VG Base
* Has button to view Soft Deleted Flavors, and how many recipes they are in
* Can Hard Delete Soft Deleted Flavors if they are no longer in a recipe
* Can Un-Delete a Soft Deleted Flavor
* Each Flavor has a button to edit the flavor (Name, Company, PG or VG Base)
* Each Flavor has a button to delete the flavor
* When deleting a flavor,
* If flavor is in a recipe,
* Soft Delete the flavor
* Allow flavor data to be viewed in recipe, but with a strikethrough
* If flavor is not in a recipe,
* Hard Delete the flavor
* NO DUPLICATES ALLOWED