mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-30 17:53:31 -04:00 
			
		
		
		
	feat: Add the ability to flag a food as "on hand", to exclude from shopping list (#3777)
This commit is contained in:
		| @@ -0,0 +1,48 @@ | ||||
| """Add staple flag to foods | ||||
|  | ||||
| Revision ID: 32d69327997b | ||||
| Revises: 7788478a0338 | ||||
| Create Date: 2024-06-22 10:17:03.323966 | ||||
|  | ||||
| """ | ||||
|  | ||||
| import sqlalchemy as sa | ||||
| from sqlalchemy import orm | ||||
|  | ||||
| from alembic import op | ||||
|  | ||||
|  | ||||
| # revision identifiers, used by Alembic. | ||||
| revision = "32d69327997b" | ||||
| down_revision = "7788478a0338" | ||||
| branch_labels = None | ||||
| depends_on = None | ||||
|  | ||||
|  | ||||
| def is_postgres(): | ||||
|     return op.get_context().dialect.name == "postgresql" | ||||
|  | ||||
|  | ||||
| def upgrade(): | ||||
|     with op.batch_alter_table("ingredient_foods") as batch_op: | ||||
|         batch_op.add_column(sa.Column("on_hand", sa.Boolean(), nullable=True, default=False)) | ||||
|  | ||||
|     bind = op.get_bind() | ||||
|     session = orm.Session(bind=bind) | ||||
|  | ||||
|     with session: | ||||
|         if is_postgres(): | ||||
|             stmt = "UPDATE ingredient_foods SET on_hand = FALSE;" | ||||
|         else: | ||||
|             stmt = "UPDATE ingredient_foods SET on_hand = 0;" | ||||
|  | ||||
|         session.execute(sa.text(stmt)) | ||||
|  | ||||
|     # forbid nulls after migration | ||||
|     with op.batch_alter_table("ingredient_foods") as batch_op: | ||||
|         batch_op.alter_column("on_hand", nullable=False) | ||||
|  | ||||
|  | ||||
| def downgrade(): | ||||
|     with op.batch_alter_table("ingredient_foods") as batch_op: | ||||
|         batch_op.drop_column("on_hand") | ||||
| @@ -231,7 +231,7 @@ export default defineComponent({ | ||||
|  | ||||
|         const shoppingListIngredients: ShoppingListIngredient[] = recipe.recipeIngredient.map((ing) => { | ||||
|           return { | ||||
|             checked: true, | ||||
|             checked: !ing.food?.onHand, | ||||
|             ingredient: ing, | ||||
|             disableAmount: recipe.settings?.disableAmount || false, | ||||
|           } | ||||
|   | ||||
| @@ -19,6 +19,7 @@ export const useFoodData = function () { | ||||
|     name: "", | ||||
|     description: "", | ||||
|     labelId: undefined, | ||||
|     onHand: false, | ||||
|   }); | ||||
|  | ||||
|   function reset() { | ||||
| @@ -26,6 +27,7 @@ export const useFoodData = function () { | ||||
|     data.name = ""; | ||||
|     data.description = ""; | ||||
|     data.labelId = undefined; | ||||
|     data.onHand = false; | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|   | ||||
| @@ -988,7 +988,8 @@ | ||||
|       "food-data": "Food Data", | ||||
|       "example-food-singular": "ex: Onion", | ||||
|       "example-food-plural": "ex: Onions", | ||||
|       "label-overwrite-warning": "This will assign the chosen label to all selected foods and potentially overwrite your existing labels." | ||||
|       "label-overwrite-warning": "This will assign the chosen label to all selected foods and potentially overwrite your existing labels.", | ||||
|       "on-hand-checkbox-label": "Setting this flag will make this food unchecked by default when adding a recipe to a shopping list." | ||||
|     }, | ||||
|     "units": { | ||||
|       "seed-dialog-text": "Seed the database with common units based on your local language.", | ||||
|   | ||||
| @@ -63,6 +63,7 @@ export interface CreateIngredientFood { | ||||
|   }; | ||||
|   labelId?: string; | ||||
|   aliases?: CreateIngredientFoodAlias[]; | ||||
|   onHand?: boolean; | ||||
| } | ||||
| export interface CreateIngredientFoodAlias { | ||||
|   name: string; | ||||
| @@ -135,6 +136,7 @@ export interface IngredientFood { | ||||
|   label?: MultiPurposeLabelSummary; | ||||
|   createdAt?: string; | ||||
|   updateAt?: string; | ||||
|   onHand?: boolean; | ||||
| } | ||||
| export interface IngredientFoodAlias { | ||||
|   name: string; | ||||
|   | ||||
| @@ -87,6 +87,14 @@ | ||||
|             :label="$t('data-pages.foods.food-label')" | ||||
|           > | ||||
|           </v-autocomplete> | ||||
|           <v-checkbox | ||||
|             v-model="createTarget.onHand" | ||||
|             hide-details | ||||
|             :label="$t('tool.on-hand')" | ||||
|           /> | ||||
|           <p class="text-caption mt-1"> | ||||
|             {{ $t("data-pages.foods.on-hand-checkbox-label") }} | ||||
|           </p> | ||||
|         </v-form> </v-card-text | ||||
|     ></BaseDialog> | ||||
|  | ||||
| @@ -134,6 +142,14 @@ | ||||
|             :label="$t('data-pages.foods.food-label')" | ||||
|           > | ||||
|           </v-autocomplete> | ||||
|           <v-checkbox | ||||
|             v-model="editTarget.onHand" | ||||
|             hide-details | ||||
|             :label="$t('tool.on-hand')" | ||||
|           /> | ||||
|           <p class="text-caption mt-1"> | ||||
|             {{ $t("data-pages.foods.on-hand-checkbox-label") }} | ||||
|           </p> | ||||
|         </v-form> | ||||
|       </v-card-text> | ||||
|       <template #custom-card-action> | ||||
| @@ -243,6 +259,11 @@ | ||||
|           {{ item.label.name }} | ||||
|         </MultiPurposeLabel> | ||||
|       </template> | ||||
|       <template #item.onHand="{ item }"> | ||||
|         <v-icon :color="item.onHand ? 'success' : undefined"> | ||||
|           {{ item.onHand ? $globals.icons.check : $globals.icons.close }} | ||||
|         </v-icon> | ||||
|       </template> | ||||
|       <template #button-bottom> | ||||
|         <BaseButton @click="seedDialog = true"> | ||||
|           <template #icon> {{ $globals.icons.database }} </template> | ||||
| @@ -300,6 +321,11 @@ export default defineComponent({ | ||||
|         value: "label", | ||||
|         show: true, | ||||
|       }, | ||||
|       { | ||||
|         text: i18n.tc("tool.on-hand"), | ||||
|         value: "onHand", | ||||
|         show: true, | ||||
|       }, | ||||
|     ]; | ||||
|  | ||||
|     const foodStore = useFoodStore(); | ||||
|   | ||||
| @@ -36,7 +36,9 @@ class IngredientUnitModel(SqlAlchemyBase, BaseMixins): | ||||
|         "RecipeIngredientModel", back_populates="unit" | ||||
|     ) | ||||
|     aliases: Mapped[list["IngredientUnitAliasModel"]] = orm.relationship( | ||||
|         "IngredientUnitAliasModel", back_populates="unit", cascade="all, delete, delete-orphan" | ||||
|         "IngredientUnitAliasModel", | ||||
|         back_populates="unit", | ||||
|         cascade="all, delete, delete-orphan", | ||||
|     ) | ||||
|  | ||||
|     # Automatically updated by sqlalchemy event, do not write to this manually | ||||
| @@ -144,12 +146,15 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins): | ||||
|     name: Mapped[str | None] = mapped_column(String) | ||||
|     plural_name: Mapped[str | None] = mapped_column(String) | ||||
|     description: Mapped[str | None] = mapped_column(String) | ||||
|     on_hand: Mapped[bool] = mapped_column(Boolean) | ||||
|  | ||||
|     ingredients: Mapped[list["RecipeIngredientModel"]] = orm.relationship( | ||||
|         "RecipeIngredientModel", back_populates="food" | ||||
|     ) | ||||
|     aliases: Mapped[list["IngredientFoodAliasModel"]] = orm.relationship( | ||||
|         "IngredientFoodAliasModel", back_populates="food", cascade="all, delete, delete-orphan" | ||||
|         "IngredientFoodAliasModel", | ||||
|         back_populates="food", | ||||
|         cascade="all, delete, delete-orphan", | ||||
|     ) | ||||
|     extras: Mapped[list[IngredientFoodExtras]] = orm.relationship("IngredientFoodExtras", cascade="all, delete-orphan") | ||||
|  | ||||
| @@ -162,7 +167,13 @@ class IngredientFoodModel(SqlAlchemyBase, BaseMixins): | ||||
|  | ||||
|     @api_extras | ||||
|     @auto_init() | ||||
|     def __init__(self, session: Session, name: str | None = None, plural_name: str | None = None, **_) -> None: | ||||
|     def __init__( | ||||
|         self, | ||||
|         session: Session, | ||||
|         name: str | None = None, | ||||
|         plural_name: str | None = None, | ||||
|         **_, | ||||
|     ) -> None: | ||||
|         if name is not None: | ||||
|             self.name_normalized = self.normalize(name) | ||||
|         if plural_name is not None: | ||||
| @@ -317,7 +328,13 @@ class RecipeIngredientModel(SqlAlchemyBase, BaseMixins): | ||||
|     original_text_normalized: Mapped[str | None] = mapped_column(String, index=True) | ||||
|  | ||||
|     @auto_init() | ||||
|     def __init__(self, session: Session, note: str | None = None, orginal_text: str | None = None, **_) -> None: | ||||
|     def __init__( | ||||
|         self, | ||||
|         session: Session, | ||||
|         note: str | None = None, | ||||
|         orginal_text: str | None = None, | ||||
|         **_, | ||||
|     ) -> None: | ||||
|         # SQLAlchemy events do not seem to register things that are set during auto_init | ||||
|         if note is not None: | ||||
|             self.note_normalized = self.normalize(note) | ||||
|   | ||||
| @@ -36,6 +36,7 @@ class UnitFoodBase(MealieModel): | ||||
|     plural_name: str | None = None | ||||
|     description: str = "" | ||||
|     extras: dict | None = {} | ||||
|     on_hand: bool = False | ||||
|  | ||||
|     @field_validator("id", mode="before") | ||||
|     def convert_empty_id_to_none(cls, v): | ||||
| @@ -79,13 +80,19 @@ class IngredientFood(CreateIngredientFood): | ||||
|     created_at: datetime.datetime | None = None | ||||
|     update_at: datetime.datetime | None = None | ||||
|  | ||||
|     _searchable_properties: ClassVar[list[str]] = ["name_normalized", "plural_name_normalized"] | ||||
|     _searchable_properties: ClassVar[list[str]] = [ | ||||
|         "name_normalized", | ||||
|         "plural_name_normalized", | ||||
|     ] | ||||
|     _normalize_search: ClassVar[bool] = True | ||||
|     model_config = ConfigDict(from_attributes=True) | ||||
|  | ||||
|     @classmethod | ||||
|     def loader_options(cls) -> list[LoaderOption]: | ||||
|         return [joinedload(IngredientFoodModel.extras), joinedload(IngredientFoodModel.label)] | ||||
|         return [ | ||||
|             joinedload(IngredientFoodModel.extras), | ||||
|             joinedload(IngredientFoodModel.label), | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class IngredientFoodPagination(PaginationBase): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user