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 import uuid
from datetime import date, datetime, time, timedelta 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 fastapi import APIRouter, Cookie, Depends, Security
from sqlalchemy import func, nulls_last, or_, select 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 import Voucher
from ...models.voucher_type import VoucherType from ...models.voucher_type import VoucherType
from ...printing.product_sale_report import print_product_sale_report 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 ...schemas.user_token import UserToken
from . import check_audit_permission, report_finish_date, report_start_date 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 = APIRouter()
@router.get("") @router.get("", response_model=MeReport)
def menu_engineering_report_view( def menu_engineering_report_view(
start_date: date = Depends(report_start_date), start_date: date = Depends(report_start_date),
finish_date: date = Depends(report_finish_date), finish_date: date = Depends(report_finish_date),
@ -34,11 +36,11 @@ def menu_engineering_report_view(
): ):
check_audit_permission(start_date, user.permissions) check_audit_permission(start_date, user.permissions)
with SessionFuture() as db: with SessionFuture() as db:
return { return MeReport(
"startDate": start_date.strftime("%d-%b-%Y"), startDate=start_date,
"finishDate": finish_date.strftime("%d-%b-%Y"), finishDate=finish_date,
"amounts": menu_engineering_report(start_date, finish_date, db), amounts=menu_engineering_report(start_date, finish_date, db),
} )
def menu_engineering_report(s: date, f: date, db: Session): 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) print(q)
list_ = db.execute(q).all() list_ = db.execute(q).all()
info: list[Any] = [] info: list[MeItem] = []
for sc, mc, id_, name, price, quantity, amount in list_: 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( info.append(
{ MeItem(
"id": id_, id=id_,
"name": name, name=name,
"price": price, price=price,
"average": round(amount / quantity) if amount and quantity else price, average=round(sales / quantity) if sales and quantity else price,
"saleCategory": sc, saleCategory=sc,
"menuCategory": mc, menuCategory=mc,
"quantity": quantity, quantity=quantity or Decimal(0),
"amount": amount, 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 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) return merge(observableOf(this.data), this.filter, ...dataMutations)
.pipe( .pipe(
map(() => this.getFilteredData([...this.data])), map(() => this.getFilteredData([...this.data])),
map((x) => this.recalculate(x)),
tap((x: MenuEngineeringReportItem[]) => { tap((x: MenuEngineeringReportItem[]) => {
if (this.paginator) { if (this.paginator) {
this.paginator.length = x.length; 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'] ?? ''); return math_it_up(result.groups['sign'])(x.quantity, +result.groups['amount'] ?? '');
} }
} }
if (c.startsWith('a:')) { if (c.startsWith('s:')) {
const result = c.match(/^a:(?<sign>=|<=|>=|<|>)(?<amount>\d*)$/); const result = c.match(/^s:(?<sign>=|<=|>=|<|>)(?<amount>\d*)$/);
if (result && result.groups) { 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(); 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[]) { private getPagedData(data: MenuEngineeringReportItem[]) {
if (this.paginator === undefined) { if (this.paginator === undefined) {
return data; return data;
@ -110,8 +127,8 @@ export class MenuEngineeringReportDataSource extends DataSource<MenuEngineeringR
return compare(a.menuCategory, b.menuCategory, isAsc); return compare(a.menuCategory, b.menuCategory, isAsc);
case 'quantity': case 'quantity':
return compare(a.quantity, b.quantity, isAsc); return compare(a.quantity, b.quantity, isAsc);
case 'amount': case 'sales':
return compare(a.amount, b.amount, isAsc); return compare(a.sales, b.sales, isAsc);
default: default:
return 0; return 0;
} }
@ -120,7 +137,7 @@ export class MenuEngineeringReportDataSource extends DataSource<MenuEngineeringR
} }
function eq(x: number, y: number) { function eq(x: number, y: number) {
return x > y; return x === y;
} }
function gt(x: number, y: number) { function gt(x: number, y: number) {
return x > y; return x > y;

View File

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

View File

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

View File

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