Product Detail is neater.

Recipe list fixed.

Recipe xlsx not working.
This commit is contained in:
2025-07-21 06:18:06 +00:00
parent 0a60a4be6a
commit 12bddcd7cc
10 changed files with 77 additions and 35 deletions

View File

@ -119,7 +119,7 @@ def save(
def date_range( def date_range(
start: date, stop: date, step: timedelta = timedelta(days=1), inclusive: bool = False start: date, stop: date, step: timedelta = timedelta(days=1), inclusive: bool = False
) -> Generator[date, None, None]: ) -> Generator[date]:
# inclusive=False to behave like range by default # inclusive=False to behave like range by default
if step.days > 0: if step.days > 0:
while start < stop: while start < stop:

View File

@ -125,7 +125,9 @@ async def update_route(
item.quantity = round(new_item.quantity, 2) item.quantity = round(new_item.quantity, 2)
item.description = new_item.description item.description = new_item.description
else: else:
db.delete(item)
recipe.items.remove(item) recipe.items.remove(item)
db.flush()
for d_item in data.items: for d_item in data.items:
product = db.execute(select(Product).where(Product.id == d_item.product.id_)).scalar_one() product = db.execute(select(Product).where(Product.id == d_item.product.id_)).scalar_one()
@ -152,9 +154,16 @@ def check_recursion(product: uuid.UUID, visited: set[uuid.UUID], db: Session) ->
detail="Recipe recursion. Some ingredient recipe contains parent recipe.", detail="Recipe recursion. Some ingredient recipe contains parent recipe.",
) )
recipe = ( recipe = (
db.execute(select(Recipe).join(Recipe.items).join(Recipe.sku).where(StockKeepingUnit.product_id == product)) db.execute(
select(Recipe)
.join(Recipe.items)
.join(Recipe.sku)
.options(contains_eager(Recipe.items))
.where(StockKeepingUnit.product_id == product)
)
.unique() .unique()
.scalar_one_or_none() .scalars()
.first()
) )
if recipe is None: if recipe is None:
return return
@ -239,14 +248,15 @@ async def show_list(
.join(ProductVersion, onclause=product_version_onclause) .join(ProductVersion, onclause=product_version_onclause)
.join(ProductVersion.product_group) .join(ProductVersion.product_group)
.options( .options(
contains_eager(Recipe.sku).contains_eager(StockKeepingUnit.versions),
contains_eager(Recipe.sku) contains_eager(Recipe.sku)
.contains_eager(StockKeepingUnit.versions)
.contains_eager(StockKeepingUnit.product) .contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions) .contains_eager(Product.versions)
.contains_eager(ProductVersion.product_group) .contains_eager(ProductVersion.product_group),
) )
.order_by(ProductVersion.name) .order_by(ProductVersion.name)
) )
.unique()
.scalars() .scalars()
.all() .all()
) )
@ -302,15 +312,15 @@ def show_pdf(
@router.get("/xlsx", response_class=StreamingResponse) @router.get("/xlsx", response_class=StreamingResponse)
def get_report( def get_report(
p: uuid.UUID, p: uuid.UUID,
t: uuid.UUID | None = None, pg: uuid.UUID | None = None,
) -> StreamingResponse: ) -> StreamingResponse:
with SessionFuture() as db: with SessionFuture() as db:
calculate_prices(t, db) calculate_prices(p, db)
db.commit() db.commit()
prices: list[tuple[str, str, Decimal]] = [] prices: list[tuple[str, str, Decimal]] = []
with SessionFuture() as db: with SessionFuture() as db:
pq = ( pq = (
db.execute(select(Price).where(Price.period_id == t).options(joinedload(Price.product, innerjoin=True))) db.execute(select(Price).where(Price.period_id == p).options(joinedload(Price.product, innerjoin=True)))
.unique() .unique()
.scalars() .scalars()
.all() .all()
@ -318,6 +328,7 @@ def get_report(
prices = [(i.product.versions[-1].name, i.product.versions[-1].fraction_units, i.price) for i in pq] prices = [(i.product.versions[-1].name, i.product.versions[-1].fraction_units, i.price) for i in pq]
list_: Sequence[Recipe] = [] list_: Sequence[Recipe] = []
print("test2")
with SessionFuture() as db: with SessionFuture() as db:
RecipeProductVersion = aliased(ProductVersion, name="recipe_product_version") RecipeProductVersion = aliased(ProductVersion, name="recipe_product_version")
ItemProductVersion = aliased(ProductVersion, name="item_product_version") ItemProductVersion = aliased(ProductVersion, name="item_product_version")
@ -384,8 +395,8 @@ def get_report(
contains_eager(Recipe.sku).contains_eager(StockKeepingUnit.versions, alias=CurrentSkuVersion), contains_eager(Recipe.sku).contains_eager(StockKeepingUnit.versions, alias=CurrentSkuVersion),
) )
) )
if p is not None: if pg is not None:
q = q.where(RecipeProductVersion.product_group_id == p) q = q.where(RecipeProductVersion.product_group_id == pg)
list_ = db.execute(q).unique().scalars().all() list_ = db.execute(q).unique().scalars().all()
e = excel(prices, sorted(list_, key=lambda r: r.sku.product.versions[0].name)) e = excel(prices, sorted(list_, key=lambda r: r.sku.product.versions[0].name))
e.seek(0) e.seek(0)
@ -569,7 +580,8 @@ def recipe_info(recipe: Recipe) -> schemas.Recipe:
id_=item.id, id_=item.id,
product=schemas.ProductLink( product=schemas.ProductLink(
id_=item.product.id, id_=item.product.id,
name=f"{item.product.versions[0].name} ({item.product.versions[0].fraction_units})", name=item.product.versions[0].name,
fraction_units=item.product.versions[0].fraction_units,
), ),
quantity=round(item.quantity, 2), quantity=round(item.quantity, 2),
description=item.description, description=item.description,

View File

@ -12,6 +12,7 @@ from .stock_keeping_unit import StockKeepingUnit
class ProductLink(BaseModel): class ProductLink(BaseModel):
id_: uuid.UUID = Field(...) id_: uuid.UUID = Field(...)
name: str | None = None name: str | None = None
fraction_units: str | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)

View File

@ -0,0 +1,11 @@
.nutrition-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
align-items: stretch;
justify-items: stretch;
}
.nutrition-grid > * {
width: 100%;
box-sizing: border-box; /* helps with padding */
}

View File

@ -47,7 +47,7 @@
</div> </div>
@if (item.productGroup?.nutritional ?? false) { @if (item.productGroup?.nutritional ?? false) {
<h2>Nutritional Information</h2> <h2>Nutritional Information</h2>
<div class="row-container"> <div class="nutrition-grid">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Protein</mat-label> <mat-label>Protein</mat-label>
<input matInput formControlName="protein" /> <input matInput formControlName="protein" />
@ -108,7 +108,7 @@
</div> </div>
} }
<h2>Stock Keeping Units</h2> <h2>Stock Keeping Units</h2>
<div formGroupName="addRow" class="row-container space-between"> <div formGroupName="addRow" class="nutrition-grid">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Units</mat-label> <mat-label>Units</mat-label>
<input matInput formControlName="units" /> <input matInput formControlName="units" />
@ -132,7 +132,7 @@
<button mat-raised-button color="primary" (click)="addRow()" class="flex-auto">Add</button> <button mat-raised-button color="primary" (click)="addRow()" class="flex-auto">Add</button>
</div> </div>
</form> </form>
<div class="row-container"> <div class="row-container wrapped">
<mat-table [dataSource]="dataSource" aria-label="Elements" class="flex-auto"> <mat-table [dataSource]="dataSource" aria-label="Elements" class="flex-auto">
<!-- Units Column --> <!-- Units Column -->
<ng-container matColumnDef="units"> <ng-container matColumnDef="units">

View File

@ -1,6 +1,6 @@
<h2>Recipe Detail</h2> <h2>Recipe Detail</h2>
<form [formGroup]="form" class="flex-col"> <form [formGroup]="form" class="flex-col wrapped">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Date</mat-label> <mat-label>Date</mat-label>
@ -83,9 +83,13 @@
<!-- Quantity Column --> <!-- Quantity Column -->
<ng-container matColumnDef="quantity"> <ng-container matColumnDef="quantity">
<mat-header-cell *matHeaderCellDef class="right">Quantity</mat-header-cell> <mat-header-cell *matHeaderCellDef class="right">Quantity</mat-header-cell>
<mat-cell *matCellDef="let row" class="right" <mat-cell *matCellDef="let row" class="right">{{ row.quantity | number: '1.2-2' }}</mat-cell>
>{{ row.quantity | number: '1.2-2' }} {{ row.product.fractionUnits }}</mat-cell </ng-container>
>
<!-- Units Column -->
<ng-container matColumnDef="units">
<mat-header-cell *matHeaderCellDef>Units</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.product.fractionUnits }}</mat-cell>
</ng-container> </ng-container>
<!-- Action Column --> <!-- Action Column -->
@ -101,18 +105,24 @@
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row> <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table> </mat-table>
<mat-form-field class="flex-auto"> <div class="row-container">
<mat-label>Instructions</mat-label> <mat-form-field class="flex-auto">
<textarea matInput matAutosizeMinRows="5" formControlName="instructions"></textarea> <mat-label>Instructions</mat-label>
</mat-form-field> <textarea matInput matAutosizeMinRows="5" formControlName="instructions"></textarea>
<mat-form-field class="flex-auto"> </mat-form-field>
<mat-label>Garnishing</mat-label> </div>
<textarea matInput matAutosizeMinRows="5" formControlName="garnishing"></textarea> <div class="row-container">
</mat-form-field> <mat-form-field class="flex-auto">
<mat-form-field class="flex-auto"> <mat-label>Garnishing</mat-label>
<mat-label>Plating</mat-label> <textarea matInput matAutosizeMinRows="5" formControlName="garnishing"></textarea>
<textarea matInput matAutosizeMinRows="5" formControlName="plating"></textarea> </mat-form-field>
</mat-form-field> </div>
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Plating</mat-label>
<textarea matInput matAutosizeMinRows="5" formControlName="plating"></textarea>
</mat-form-field>
</div>
</form> </form>
<div class="row-container"> <div class="row-container">

View File

@ -82,7 +82,7 @@ export class RecipeDetailComponent implements OnInit, AfterViewInit {
ingredients: Observable<ProductSku[]>; ingredients: Observable<ProductSku[]>;
item: Recipe = new Recipe(); item: Recipe = new Recipe();
displayedColumns = ['product', 'quantity', 'action']; displayedColumns = ['product', 'quantity', 'units', 'action'];
constructor() { constructor() {
this.product = null; this.product = null;

View File

@ -1,6 +1,6 @@
<h2 class="row-container space-between"> <h2 class="row-container space-between">
<span>Recipes</span> <span>Recipes</span>
<a mat-icon-button [href]="'/api/recipes/xlsx?t=' + period.id"> <a mat-icon-button [href]="'/api/recipes/xlsx?p=' + period.id">
<mat-icon>save_alt</mat-icon> <mat-icon>save_alt</mat-icon>
</a> </a>
<a mat-icon-button href="/api/recipes/nutrition"> <a mat-icon-button href="/api/recipes/nutrition">
@ -16,7 +16,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-select formControlName="period"> <mat-select formControlName="period">
@for (p of periods; track p) { @for (p of periods; track p.id) {
<mat-option [value]="p"> {{ p.validFrom }} to {{ p.validTill }} </mat-option> <mat-option [value]="p"> {{ p.validFrom }} to {{ p.validTill }} </mat-option>
} }
</mat-select> </mat-select>
@ -25,7 +25,7 @@
<mat-label>Product Type</mat-label> <mat-label>Product Type</mat-label>
<mat-select formControlName="productGroup" (selectionChange)="filterProductGroup($event.value)"> <mat-select formControlName="productGroup" (selectionChange)="filterProductGroup($event.value)">
<mat-option>-- All Products --</mat-option> <mat-option>-- All Products --</mat-option>
@for (mc of productGroups; track mc) { @for (mc of productGroups; track mc.id) {
<mat-option [value]="mc.id"> <mat-option [value]="mc.id">
{{ mc.name }} {{ mc.name }}
</mat-option> </mat-option>

View File

@ -69,12 +69,13 @@ export class RecipeListComponent implements OnInit {
period: new FormControl(new Period(), { nonNullable: true }), period: new FormControl(new Period(), { nonNullable: true }),
productGroup: new FormControl<ProductGroup | string | null>(null), productGroup: new FormControl<ProductGroup | string | null>(null),
}); });
// Listen to Payment Account Change // Listen to Period Change
this.form.controls.period.valueChanges.subscribe((x) => { this.form.controls.period.valueChanges.subscribe((x) => {
this.router.navigate([], { this.router.navigate([], {
relativeTo: this.route, relativeTo: this.route,
queryParams: { p: x.id }, queryParams: { p: x.id },
replaceUrl: true, replaceUrl: true,
queryParamsHandling: 'merge',
}); });
this.period = x; this.period = x;
}); });

View File

@ -81,6 +81,13 @@
.center { .center {
text-align: center; text-align: center;
} }
.right {
display: flex;
justify-content: flex-end;
}
// .right {
// text-align: right;
// }
.warn { .warn {
background-color: red; background-color: red;