Feature: The menu engineering report now recalculates the percentages based on filter

This commit is contained in:
Amritanshu Agrawal 2023-03-19 09:56:48 +05:30
parent 4e4cebf5d4
commit c14e64ce85
6 changed files with 126 additions and 35 deletions

View File

@ -1,7 +1,8 @@
import uuid
from datetime import date, datetime, time, timedelta
from typing import Any
from decimal import Decimal
from typing import Dict
from fastapi import APIRouter, Cookie, Depends, Security
from sqlalchemy import func, nulls_last, or_, select
@ -19,6 +20,7 @@ from ...models.sale_category import SaleCategory
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
from ...printing.product_sale_report import print_product_sale_report
from ...schemas.menu_engineering_report import MeItem, MeReport
from ...schemas.user_token import UserToken
from . import check_audit_permission, report_finish_date, report_start_date
@ -26,7 +28,7 @@ from . import check_audit_permission, report_finish_date, report_start_date
router = APIRouter()
@router.get("")
@router.get("", response_model=MeReport)
def menu_engineering_report_view(
start_date: date = Depends(report_start_date),
finish_date: date = Depends(report_finish_date),
@ -34,11 +36,11 @@ def menu_engineering_report_view(
):
check_audit_permission(start_date, user.permissions)
with SessionFuture() as db:
return {
"startDate": start_date.strftime("%d-%b-%Y"),
"finishDate": finish_date.strftime("%d-%b-%Y"),
"amounts": menu_engineering_report(start_date, finish_date, db),
}
return MeReport(
startDate=start_date,
finishDate=finish_date,
amounts=menu_engineering_report(start_date, finish_date, db),
)
def menu_engineering_report(s: date, f: date, db: Session):
@ -100,20 +102,36 @@ def menu_engineering_report(s: date, f: date, db: Session):
)
print(q)
list_ = db.execute(q).all()
info: list[Any] = []
for sc, mc, id_, name, price, quantity, amount in list_:
info: list[MeItem] = []
sc_sales: Dict[str, Decimal] = {}
sc_quantity: Dict[str, Decimal] = {}
for sc, mc, id_, name, price, quantity, sales in list_:
if sc not in sc_sales:
sc_sales[sc] = Decimal(0)
if sales is not None:
sc_sales[sc] += sales
if sc not in sc_quantity:
sc_quantity[sc] = Decimal(0)
if quantity is not None:
sc_quantity[sc] += quantity
info.append(
{
"id": id_,
"name": name,
"price": price,
"average": round(amount / quantity) if amount and quantity else price,
"saleCategory": sc,
"menuCategory": mc,
"quantity": quantity,
"amount": amount,
}
MeItem(
id=id_,
name=name,
price=price,
average=round(sales / quantity) if sales and quantity else price,
saleCategory=sc,
menuCategory=mc,
quantity=quantity or Decimal(0),
sales=sales or Decimal(0),
quantityPercent=Decimal(0),
salesPercent=Decimal(0),
)
)
for item in info:
if sc_quantity[item.sale_category] > 0:
item.quantity_percent = round(item.quantity / sc_quantity[item.sale_category], 4)
item.sales_percent = round(item.sales / sc_sales[item.sale_category], 4)
return info

View File

@ -0,0 +1,48 @@
import uuid
from datetime import date, datetime
from decimal import Decimal
from pydantic import BaseModel, validator
from . import to_camel
class MeItem(BaseModel):
id_: uuid.UUID
name: str
price: Decimal
average: Decimal
sale_category: str
menu_category: str
quantity: Decimal
sales: Decimal
quantity_percent: Decimal
sales_percent: Decimal
class Config:
anystr_strip_whitespace = True
alias_generator = to_camel
class MeReport(BaseModel):
start_date: date
finish_date: date
amounts: list[MeItem]
class Config:
anystr_strip_whitespace = True
alias_generator = to_camel
json_encoders = {datetime: lambda v: v.strftime("%d-%b-%Y %H:%M"), date: lambda v: v.strftime("%d-%b-%Y")}
@validator("start_date", pre=True)
def parse_start_date(cls, value):
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@validator("finish_date", pre=True)
def parse_finish_date(cls, value):
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()

View File

@ -37,6 +37,7 @@ export class MenuEngineeringReportDataSource extends DataSource<MenuEngineeringR
return merge(observableOf(this.data), this.filter, ...dataMutations)
.pipe(
map(() => this.getFilteredData([...this.data])),
map((x) => this.recalculate(x)),
tap((x: MenuEngineeringReportItem[]) => {
if (this.paginator) {
this.paginator.length = x.length;
@ -67,10 +68,10 @@ export class MenuEngineeringReportDataSource extends DataSource<MenuEngineeringR
return math_it_up(result.groups['sign'])(x.quantity, +result.groups['amount'] ?? '');
}
}
if (c.startsWith('a:')) {
const result = c.match(/^a:(?<sign>=|<=|>=|<|>)(?<amount>\d*)$/);
if (c.startsWith('s:')) {
const result = c.match(/^s:(?<sign>=|<=|>=|<|>)(?<amount>\d*)$/);
if (result && result.groups) {
return math_it_up(result.groups['sign'])(x.amount, +result.groups['amount'] ?? '');
return math_it_up(result.groups['sign'])(x.sales, +result.groups['amount'] ?? '');
}
}
const itemString = `${x.name} ${x.saleCategory} ${x.menuCategory}`.toLowerCase();
@ -80,6 +81,22 @@ export class MenuEngineeringReportDataSource extends DataSource<MenuEngineeringR
);
}
private recalculate(data: MenuEngineeringReportItem[]): MenuEngineeringReportItem[] {
const gr = data.reduce(function (res: Record<string, { q: number; s: number }>, value: MenuEngineeringReportItem) {
if (!res[value['saleCategory']]) {
res[value['saleCategory']] = { q: 0, s: 0 };
}
res[value['saleCategory']].q += value.quantity;
res[value['saleCategory']].s += value.sales;
return res;
}, {});
data.forEach((v) => {
v.quantityPercent = v.quantity / gr[v.saleCategory].q;
v.salesPercent = v.sales / gr[v.saleCategory].s;
});
return data;
}
private getPagedData(data: MenuEngineeringReportItem[]) {
if (this.paginator === undefined) {
return data;
@ -110,8 +127,8 @@ export class MenuEngineeringReportDataSource extends DataSource<MenuEngineeringR
return compare(a.menuCategory, b.menuCategory, isAsc);
case 'quantity':
return compare(a.quantity, b.quantity, isAsc);
case 'amount':
return compare(a.amount, b.amount, isAsc);
case 'sales':
return compare(a.sales, b.sales, isAsc);
default:
return 0;
}
@ -120,7 +137,7 @@ export class MenuEngineeringReportDataSource extends DataSource<MenuEngineeringR
}
function eq(x: number, y: number) {
return x > y;
return x === y;
}
function gt(x: number, y: number) {
return x > y;

View File

@ -6,7 +6,9 @@ export class MenuEngineeringReportItem {
saleCategory: string;
menuCategory: string;
quantity: number;
amount: number;
quantityPercent: number;
sales: number;
salesPercent: number;
public constructor(init?: Partial<MenuEngineeringReportItem>) {
this.id = '';
@ -16,7 +18,9 @@ export class MenuEngineeringReportItem {
this.saleCategory = '';
this.menuCategory = '';
this.quantity = 0;
this.amount = 0;
this.quantityPercent = 0;
this.sales = 0;
this.salesPercent = 0;
Object.assign(this, init);
}
}

View File

@ -46,7 +46,7 @@
<input type="text" matInput #filterElement formControlName="filter" autocomplete="off" />
<mat-hint
>n: Name, sc: Sale Category, mc: Menu Category, q:(=/&lt;=/&gt;=/&lt;/&gt;)Quantity,
a:(=/&lt;=/&gt;=/&lt;/&gt;)Amount</mat-hint
s:(=/&lt;=/&gt;=/&lt;/&gt;)Sales</mat-hint
>
</mat-form-field>
</div>
@ -81,13 +81,17 @@
<!-- Quantity Column -->
<ng-container matColumnDef="quantity">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Quantity</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row.quantity | number : '1.2-2' }}</mat-cell>
<mat-cell *matCellDef="let row" class="right"
>{{ row.quantity | number : '1.2-2' }} ({{ row.quantityPercent | percent : '1.2-2' }})</mat-cell
>
</ng-container>
<!-- Amount Column -->
<ng-container matColumnDef="amount">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row.amount | number : '1.2-2' }}</mat-cell>
<!-- Sales Column -->
<ng-container matColumnDef="sales">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Sales</mat-header-cell>
<mat-cell *matCellDef="let row" class="right"
>{{ row.sales | number : '1.2-2' }} ({{ row.salesPercent | percent : '1.2-2' }})</mat-cell
>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>

View File

@ -33,7 +33,7 @@ export class MenuEngineeringReportComponent implements OnInit {
}>;
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['name', 'price', 'saleCategory', 'menuCategory', 'quantity', 'amount'];
displayedColumns = ['name', 'price', 'saleCategory', 'menuCategory', 'quantity', 'sales'];
constructor(
private route: ActivatedRoute,
@ -104,7 +104,7 @@ export class MenuEngineeringReportComponent implements OnInit {
'Sale Category': 'saleCategory',
'Menu Category': 'menuCategory',
Quantity: 'quantity',
Amount: 'amount',
Sales: 'sales',
};
const csvData = new Blob([this.toCsv.toCsv(headers, this.dataSource.data)], {
type: 'text/csv;charset=utf-8;',