Feature: The menu engineering report now recalculates the percentages based on filter
This commit is contained in:
parent
4e4cebf5d4
commit
c14e64ce85
@ -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
|
||||
|
||||
|
||||
|
48
barker/barker/schemas/menu_engineering_report.py
Normal file
48
barker/barker/schemas/menu_engineering_report.py
Normal 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()
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@
|
||||
<input type="text" matInput #filterElement formControlName="filter" autocomplete="off" />
|
||||
<mat-hint
|
||||
>n: Name, sc: Sale Category, mc: Menu Category, q:(=/<=/>=/</>)Quantity,
|
||||
a:(=/<=/>=/</>)Amount</mat-hint
|
||||
s:(=/<=/>=/</>)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>
|
||||
|
@ -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;',
|
||||
|
Loading…
Reference in New Issue
Block a user